Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4주차 과제 #4

Merged
merged 41 commits into from
Dec 6, 2024
Merged

4주차 과제 #4

merged 41 commits into from
Dec 6, 2024

Conversation

ThirFir
Copy link
Contributor

@ThirFir ThirFir commented Nov 14, 2024

Work Description ✏️

  • 회원가입, 로그인, 취미 검색, 프로필 수정 API 연동
  • Base url -> local.properties
  • 토큰 저장 (EncryptedSharedPreferences)
  • API Response 파싱 (Retrofit Converter, Interceptor)
  • 에러 분기
  • 자동 로그인
  • 검색 디바운싱 및 로딩

Screenshot 📸

default.mp4
default.mp4
default.mp4

To Reviewers 📢

UI 직접 구성하는 건 너무 어렵네요 ㅠ
과제 요구사항에 대한 최소한으로만 구성했습니다.
Request 헤더는 현재 수동으로 넣어주고 있는데, 추후에 Interceptor에 chaining 해보겠습니다.

Copy link

@0se0 0se0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고많으셨습니다~!
합세 파이팅 🍀

Comment on lines +7 to +24
class TokenLocalDataSource @Inject constructor(
@TokenEncryptedSharedPref private val tokenEncryptedSharedPref: SharedPreferences
) {

fun saveToken(token: String) {
tokenEncryptedSharedPref.edit()
.putString(TOKEN, token)
.apply()
}

fun getToken(): String {
return tokenEncryptedSharedPref.getString(TOKEN, "").orEmpty()
}

companion object {
private const val TOKEN = "token"
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

최고다

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

최고십니다.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

최고짱짱맨이십니다

Copy link
Contributor Author

@ThirFir ThirFir Nov 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네?
감사합니다.

Comment on lines +28 to +31
private val userLocalDataSource: UserLocalDataSource,
private val userRemoteDataSource: UserRemoteDataSource,
private val tokenLocalDataSource: TokenLocalDataSource,
@IODispatcher private val ioDispatcher: CoroutineDispatcher
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

분리를 기가맥히게 잘하셧네요

Comment on lines +6 to +10
@Serializable
data class SignInRequest(
@SerialName("username") val username: String,
@SerialName("password") val password: String
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

원래 이렇게 한줄로 구현하시는게 취향이신가요?(정말 궁금해서..)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵! 저는 변수에 대한 어노테이션은 강경 한줄파입니다 ! (너무 긴 거 아니면..)

Copy link
Member

@Roel4990 Roel4990 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다!

plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.kapt)
alias(libs.plugins.hilt)
kotlin("plugin.serialization") version "2.0.20"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 추가할 수도 있군요!
위 코드 처럼 alias(libs.plugins.kotlin.serialization) 이런식으로 추가해주는게 어떨까요..?!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정했습니다 !

@@ -18,6 +25,7 @@ android {
versionName = "1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "BASE_URL", "String.valueOf(\"${localProperties["base_url"]}\")")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"String.valueOf("${localProperties["base_url"]}")" => localProperties["base_url"].toString() 로 작성하는건 어떨까요?!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

local.properties에 키 작성할 때 base_url="qwerasdf1234" 이런식으로 따옴표를 붙일 수 있지만, 저는 따옴표를 붙이지않고 base_url=qwerasdf1234 이렇게 쓰고 싶었습니다 !
그런데 따옴표를 붙이면 toString()으로 되는데, 따옴표를 붙이지 않으면 toString으로 되진 않고 저 방식으로만 해야 했습니다 !!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

따옴표 붙이지 않는 이유가 있나요? 궁금해요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

manifestPlaceholders로 Manifest에 변수 넣을 때는 따옴표를 넣으면 안되더라구여. 그래서 저는 통일성을 위해 전부 따옴표 없이 사용합니당
(지금은 상관없긴하지만..!)

Comment on lines +23 to +47
fun provideUserSharedPreferences(
@ApplicationContext context: Context
): SharedPreferences {
return context.getSharedPreferences("user.pref", Context.MODE_PRIVATE)
}

@TokenEncryptedSharedPref
@Provides
@Singleton
fun provideTokenEncryptedSharedPreferences(
@ApplicationContext context: Context
): SharedPreferences {
val masterKeyAlias = MasterKey
.Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()

return EncryptedSharedPreferences.create(
context,
"token.pref.enc",
masterKeyAlias,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 좋네요. 암호화된 SharedPreferences 저도 다음에 한번 써봐야겠군요..

Comment on lines +64 to +77
@Singleton
@Provides
fun provideRetrofit(
client: OkHttpClient
): Retrofit {
val json = Json { ignoreUnknownKeys = true }
return Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.client(client)
.addConverterFactory(ResultConverterFactory())
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

val json = Json { ignoreUnknownKeys = true } 이거 어떤이유로 사용한건지 여쭤봐도 될까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

데이터 클래스로 지정해두지 않은 필드가 API 통신을 통해 들어왔을 때, 그 필드를 무시하는 속성입니다 !
이번 과제에선 큰 의미는 없는데, 그냥 써봤습니다 ㅎㅎ

Comment on lines +7 to +24
class TokenLocalDataSource @Inject constructor(
@TokenEncryptedSharedPref private val tokenEncryptedSharedPref: SharedPreferences
) {

fun saveToken(token: String) {
tokenEncryptedSharedPref.edit()
.putString(TOKEN, token)
.apply()
}

fun getToken(): String {
return tokenEncryptedSharedPref.getString(TOKEN, "").orEmpty()
}

companion object {
private const val TOKEN = "token"
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

최고십니다.

import kotlin.coroutines.cancellation.CancellationException

suspend fun <T, R> T.runCatchingByCode(
vararg exceptions: Pair<Int, Throwable>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vararg 는 처음보네요.. 공부하겠습니다.

Comment on lines +5 to +39
data class NetworkError(
val statusCode: Int,
val errorCode: Int,
override val message: String?
) : IOException()

sealed class SignInError : Throwable() {
class NotExistUsername() : SignInError()
class PasswordNotMatchingWithUsername() : SignInError()
class UsernameInputEmpty() : SignInError()
class PasswordInputEmpty() : SignInError()
}

sealed class SignUpError : Throwable() {
class InvalidUsername() : SignUpError()
class InvalidPassword() : SignUpError()
class InvalidHobby() : SignUpError()
class UsernameInputEmpty() : SignUpError()
class PasswordInputEmpty() : SignUpError()
class HobbyInputEmpty() : SignUpError()
class AlreadyExistUsername() : SignUpError()
}

sealed class SearchHobbyError : Throwable() {
class InputEmpty : SearchHobbyError()
class InputNotNumber : SearchHobbyError()
class NotExistUserNo : SearchHobbyError()
}

sealed class UpdateProfileError : Throwable() {
class InvalidPassword() : UpdateProfileError()
class InvalidHobby() : UpdateProfileError()
class PasswordInputEmpty() : UpdateProfileError()
class HobbyInputEmpty() : UpdateProfileError()
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

깔끔하네요..!

Copy link

@KkamSonLee KkamSonLee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다! 공통화를 정말 열심히 하시려고 노력하신 것 같네요 ㅎㅎ 이전에 언급드린 부분도 수정된 것 확인했습니다!

annotations: Array<out Annotation>,
retrofit: Retrofit
): Converter<ResponseBody, *> {
val json = Json { ignoreUnknownKeys = true }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

coerceInputValues, encodeDefaults, isLenient 등등 option 값들이 정말 많습니다! 해당 옵션들이 정말 필요한 경우가 많기 때문에 한 번 알아보시면 좋을 것 같아요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

직렬화가 공부할 게 참 많은 것 같습니다..!
더 학습해볼게요 !!

.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

level 은 항상 BODY 를 출력하는게 좋진 않을 것 같아요! 개발을 하는 동안 DEBUG 단계에서만 출력 되어도 좋을 것 같습니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BuildConfig로 구분하겠습니다 !

Comment on lines +32 to +34
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 �timeout 이 각각 10, 30, 15인 이유가 있을까요?? 지금은 상관 없지만 파일 같은걸 업로드 하는 데에 writeTimeout 이 15초로 부족할 수도 있긴합니다! (물론 그 때는 업로드용 OkhttpClient를 timeOut이 널널하게 만들면 좋겠죠!)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

딱히 큰 의미를 둔 건 아닙니다 ㅎㅎ..
이것도 적정 시간 같은 게 있는지 더 알아봐야 할 것 같습니다 🫠


@Serializable
data class SignUpDto(
@SerialName("no") val no: Int

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다른 Dto 들은 nullable 하게 설계하신 것 같은데 해당 필드는 nullable 하지 않은 이유가 있나요??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아앗.. 미스입니다 !
지정해두겠습니다

Comment on lines +35 to +44
private val myHobby: StateFlow<String> = flow {
val token = tokenLocalDataSource.getToken()
_myHobby.value = userRemoteDataSource.fetchMyHobby(token).hobby ?: ""

emitAll(_myHobby)
}.stateIn(
scope = CoroutineScope(ioDispatcher),
started = SharingStarted.Lazily,
initialValue = ""
)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchMyHobby() 시에 사용되도록 만드신 것 같은데 해당 코드를 fetchMyHobby() 내부에 넣어도 되지않나용?? 함수로써 쓰이는 것 같아서요! _myHobby 가 업데이트 된다고 해당 필드가 업데이트가 되는 것도 아닌 것 같아서요!
updateProfile이 되었을 때 자동적으로 fetchMyHobby() 를 collect 하고 있고, stateFlow 로 들고 있는 영역들에게 업데이트 collect가 호출 되었으면 좋겠어요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우선 위 코드 목적은 :
myHobby의 값이 여러 스크린에서 쓰이므로, 하나의 객체를 공유하는 것으로 api호출을 줄이고자 함입니다.

myHobby라는 변수로 관리한 이유는, 저 flow 블럭을 fetchMyHobby()함수 내부로 옮기면, fetchMyHobby를 호출하는 쪽은 호출할 때마다 서로 다른 새로운 flow를 제공받을 것이므로 결국 매 호출마다 api호출이 일어날 것으로 예상했습니다.
또, flow 내부에서는 emitAll에 의한, _myHobby의 업데이트 시 myHobby의 변경을 유도했습니다 !

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updateProfile 시에는 현재 flow 로 변경 사항을 받고 있는 뷰모델에서는 변화를 감지하지 못할 것 같아서요! 만일 hobby�가 계속 보여지는 화면에서 updateProfile 을 했을 때 알아서 hobby가 쓰여지고 있는 뷰가 업데이트 되어야할 때에는 fetchUserHobby 를 호출하게 될 것 같아서요! updateProfile 후에 fetchMyHobby() 함수를 호출하거나 myHobby 가 변화를 감지할 수 있었으면 좋겠습니다!!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사용측은 사실상 myHobby를 구독하는 것과 마찬가지이므로, updateProfile 호출 후에는 현재 모든 사용측에서 자동으로 변화를 감지하고 있습니다! (stateIn으로 repository로부터 값을 전달받고 있습니다)

물론, 구독은 뷰모델에서 이루어지므로 뷰모델이 소멸된 곳에서는 다시 재구독을 행하겠지만, Repository에서 사실상 싱글톤과 같은 객체로 myHobby를 관리하고 있기 때문에, 재구독이 되더라도 모든 업데이트가 반영된 최신의 값으로 받게 됩니다!!

Comment on lines +107 to +111
query.findLast { it.isDigit().not() }?.let { return }

savedStateHandle[SEARCH_QUERY] = query
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

검색어 업데이트에 해당 조건으로 return 을 하는 정책이 생긴게 있나요??

Copy link
Contributor Author

@ThirFir ThirFir Nov 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TextField에 keyboardType을 Number로 주어도 복사붙여넣기로 인한 입력은 막아지지 않는 것 같아서... 복사붙여넣기로 숫자가 아닌 것을 입력하는 것에 대한 방지입니다..!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그렇다면 입력된 값을 onValueChange 에서 filtering 하는 것도 좋을 것 같아요! 아예 입력이 되지 않게요!

// ex
currentTextValue.copy(text = currentTextValue.text.filter { it.isDigit() })

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 반영하겠습니다!

Comment on lines +97 to +108
when {
searchedHobbyUiState is SearchResultUiState.Success -> {
val hobby = (searchedHobbyUiState as SearchResultUiState.Success).hobby
PrimaryText(text = "너의 취미는 $hobby",)
}
searchedHobbyUiState is SearchResultUiState.NotExistUser -> {
PrimaryText(text = "존재하지 않는 사용자입니다.",)
}
searchedHobbyUiState is SearchResultUiState.Loading -> {
CircularProgressIndicator()
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UiState 가 5개인데 3가지만 처리되어 있는 것 같아요! 나머지도 처리 되면 좋을 것 같습니다!

Suggested change
when {
searchedHobbyUiState is SearchResultUiState.Success -> {
val hobby = (searchedHobbyUiState as SearchResultUiState.Success).hobby
PrimaryText(text = "너의 취미는 $hobby",)
}
searchedHobbyUiState is SearchResultUiState.NotExistUser -> {
PrimaryText(text = "존재하지 않는 사용자입니다.",)
}
searchedHobbyUiState is SearchResultUiState.Loading -> {
CircularProgressIndicator()
}
}
when(searchedHobbyUiState) {
is SearchResultUiState.Success -> {
val hobby = (searchedHobbyUiState as SearchResultUiState.Success).hobby
PrimaryText(text = "너의 취미는 $hobby",)
}
is SearchResultUiState.NotExistUser -> {
PrimaryText(text = "존재하지 않는 사용자입니다.",)
}
is SearchResultUiState.Loading -> {
CircularProgressIndicator()
}
// TODO other
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

감사합니다 !

import kotlin.coroutines.cancellation.CancellationException

suspend fun <T, R> T.runCatchingByCode(
vararg exceptions: Pair<Int, Throwable>,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exceptions 만을 나타내는 필드는 아닌 것 같아요! data class 로 감싸거나 이름을 바꾸면 좋을 것 같습니다! 아니면 보내는 Throwable �커스텀 클래스에 code 를 넣어두거나요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어떤 도메인에 관한 에러들을 하나의 sealed class의 자식으로 관리를 하다 보니, 일일이 생성자에 code: Int를 작성해주어야 하는 번거로움때문에 커스텀 Throwable 클래스 프로퍼티에 code를 넣어두기가 어려웠습니다😭
Code랑 Throwable을 가지는 클래스를 만드는 것도 고려해보았는데, 파라미터 넘길 때마다 새 객체를 생성해주는 행위가 사용할 때 번거로울 것 같았습니다...
그래서 나름대로 최대한으로 생각해 본 게 infix function to 를 활용할 수 있는 Pair 타입의 사용이었습니다. 사용할 때 편한 게 중요하다고 생각했습니다...!!

그런데 말씀해주신 내용에서 exceptions만을 나타내는 필드가 아닌 것 같다는 부분을 잘 이해를 못해서 정확히 어떤 의미신지 알 수 있을까요 ?!

Copy link

@KkamSonLee KkamSonLee Nov 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code는 생성자에서 입력 되는 방식이면 안 될 것 같구 고유하게 필드로 들고 있으면 될 것 같아요! 예를 들면 아래와 같이요!!

sealed class SignInError : Throwable() {
    abstract val code: Int

    class NotExistUsername : SignInError() {
        override val code: Int
            get() = 1
    }

    class PasswordNotMatchingWithUsername : SignInError() {
        override val code: Int
            get() = 2
    }

    class UsernameInputEmpty : SignInError() {
        override val code: Int
            get() = 3
    }

    class PasswordInputEmpty : SignInError() {
        override val code: Int
            get() = 4
    }
}

Pair<Int, Throwable> 에서 Int, Throwable 만으로 exception 이라는 이름의 필드가 되어야하는데 그렇게 보이지는 않아서요 사용하는 방식도 NetworkError 에만 특정 동작을 행하고 있기 때문에 함수 이름이나 파라미터명 수정이 필요해보입니다!

"catch를 하는데, NetworkError 라는 Exception이 발생했을 때 param으로 보낸 exceptions 중 NetworkError 의 code와 Pair.first()에 맞는 Pair.second()를 반환한다." 라는 동작이 코드량에 비해서 설명이 길어지는 편인데 pair 라서 조금 더 이해하기 어려울 것 같아서요!

code가 뭘 의미하는지, 왜 first()의 0, 1 일 때 second() 의 Throwable 을 반환하는지 잘 모르겠어서요 그렇다면 관련 정책은 보내는 위치에서 주석으로 적어놓던지 해야할텐데 그렇게 된다면 정말 많은 파편화가 될 것 같아요! data class 로 감싸서 code 의 필드에 대해서 내용을 기재하거나 NetworkError.kt 에서 위에 보여드린 code 필드 같은 것을 추가해서 "이 에러는 무슨 코드의 에러로 반환이 온다" 라는 것을 암시할 수 있으니까요!

"Code랑 Throwable을 가지는 클래스를 만드는 것도 고려해보았는데, 파라미터 넘길 때마다 새 객체를 생성해주는 행위가 사용할 때 번거로울 것 같았습니다..." => 기존에도 Pair 를 생성해주고 있어서 비슷할 것 같아요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀해주신대로 커스텀 예외 선언과 동시에 code를 지정하는 것이 옳다고 생각합니다!

abstract class CommonError : Throwable() {
    open val code: Int = -1
}

sealed class SignInError : CommonError() {
    class NotExistUsername : SignInError() {
        override val code: Int = 2
    }
    class PasswordNotMatchingWithUsername : SignInError() {
        override val code: Int = 1
    }
    class UsernameInputEmpty : SignInError()
    class PasswordInputEmpty : SignInError()
}

요런 느낌으로 바꿔봤습니다!

Comment on lines +74 to +79
return runCatchingByCode {
val token = tokenLocalDataSource.getToken()
userRemoteDataSource.updateProfile(token, UpdateProfileRequest(password, hobby)).also {
_myHobby.value = hobby
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

공통화도 좋지만 vararg exceptions: Pair<Int, Throwable> 를 보내지 않는다면 runCatchingByCode 함수를 안 쓰는게 더 커스텀 error 를 반환 하는 함수를 찾기 더 쉬울 것 같아요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요기서는 runCatchingByCodeCancellableException을 다시 던지기 위해 사용했습니다 !

그리고 제가 말씀해주신 것 이해를 못해서... 커스텀 error 반환 함수를 찾기 쉽다는 것이 혹시 어떤 의미인지 자세히 알 수 있을까요....?!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

함수가 여러 목적으로 사용 되는 것이 좋을 수도 있지만 해당 함수는 exceptions 로 보낸 Pair 에 대해서 �first 와 일치하는 second를 반환한다는 의미가 더 큰 것 같아서요! CancellableException 를 throw 해야한다면 함수를 2개로 나눠서 network 용과, 단순하게 CancellableException 를 throw 하는 용(network 용에서도 사용)으로 만드는게 어떨까요??

커스텀 error 반환 함수를 찾기 쉽다
=> NetworkErrkr.kt 에 있는 클래스들이 data layer 단에서 쓰이는 곳을 runCatchingByCode 함수에서만 "command + b" 로 find 해도 찾을 수 있으니까요 !

Comment on lines +82 to 86
override suspend fun saveAccount(account: Account): Result<Unit> {
return runCatchingByCode {
userLocalDataSource.saveAccount(account)
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 함수는 Local 에 있는 데이터를 접근하기 때문에 runCatchingByCode 함수로 넣을 수 있는건 Result 로 감싸져서 오는 동작만 수행 할 것 같아요(NetworkError는 올 수가 없는 구조) runCatchingByCode 는 api 호출, 즉 네트워크 호출에 필요한 함수로 보이는데 RemoteDataSource 단에서 호출하는게 어떨까요??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예리한 지적 감사합니다 !
현재는 ErrorCode가 네트워크 API 에서만 오고 있지만, 호옥시 "로컬 API에 대한 에러 코드를 만들어 사용할 수도 있지 않을까?" 라는 생각으로 위와 같이 사용했었습니다 !
그런데 생각해보니, 그러면 NetworkError라는 이름으로 지었으면 안됐을 것 같긴하네요...ㅎㅎ!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 그래서 함수를 나누는게 좋을 것 같아요!

Copy link

@chrin05 chrin05 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

과제하시느라 수고많으셨습니다!
코드 읽으면서 많이 배워갑니다!!!

interface UserApi {

@POST("user")
suspend fun signUp(@Body signUpRequest: SignUpRequest)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아예 여기서 suspend를 사용할 수도 있군요

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suspend 붙이면 Retrofit Call객체를 사용하지 않아도 돼서 편해요!!

Comment on lines +7 to +24
class TokenLocalDataSource @Inject constructor(
@TokenEncryptedSharedPref private val tokenEncryptedSharedPref: SharedPreferences
) {

fun saveToken(token: String) {
tokenEncryptedSharedPref.edit()
.putString(TOKEN, token)
.apply()
}

fun getToken(): String {
return tokenEncryptedSharedPref.getString(TOKEN, "").orEmpty()
}

companion object {
private const val TOKEN = "token"
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

최고짱짱맨이십니다

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 모듈 구현을 거의 안해봐서 처음 보는 코드들이 많은데 많이 배워갑니다..!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 사실 과제를 통해서 처음해보는 것들이 대부분입니다...!

새로운 코드에 너무 부담 안느끼고 막 다 해보는 게 좋은 것 같아요 !!

Comment on lines 16 to 18
return runCatching {
userRemoteDataSource.signUp(SignUpRequest(email, password, hobby))
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runCatching 몰랐는데 공부해봐야겠네요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코틀린이 참 다양하게 뭐가 많은것같아요 😎

Comment on lines +48 to +49
).onSuccess {
signUpUiState.emit(SignUpUiState.Success)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

state 관리하는 거 잘 이해가 안갔는데 서버 연동하면서 필요성을 많이 느낍니다

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확실히 연동이 진행될수록 구조화가 많이 중요해지는 것 같아요..!!

Copy link
Contributor

@jihyunniiii jihyunniiii left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

에러 처리 정말 야무지게 잘 해주셨네요! 배우고 갑니다 ㅋㅅㅋ 합동 세미나도 파이띵!!!

@@ -18,6 +25,7 @@ android {
versionName = "1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "BASE_URL", "String.valueOf(\"${localProperties["base_url"]}\")")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

따옴표 붙이지 않는 이유가 있나요? 궁금해요!

@ThirFir ThirFir merged commit e67afb9 into develop Dec 6, 2024
This was referenced Dec 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants