-
Notifications
You must be signed in to change notification settings - Fork 46
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
[부나] 2, 3단계 자동 DI 미션 제출합니다 #29
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
안녕하세요 부나!
오프라인으로 드렸던 피드백에 이어서 이렇게 PR 리뷰를 하게 되었네요 ㅋㅋㅋ
구조 설계하시느라 고생 많으셨습니다.
제가 드린 코멘트는 참고만 해주시고 리뷰어인 반달의 가이드를 따라가주시면 됩니다.
bunadi/build.gradle.kts
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
사용되지 않는 모듈은 제거해주세요.
di/build.gradle.kts
Outdated
sourceCompatibility = JavaVersion.VERSION_1_8 | ||
targetCompatibility = JavaVersion.VERSION_1_8 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
프로젝트 내에서 VERSION_1_8, VERSION_11이 혼용되고 있어요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오우..!
모두 11로 통일하였습니다 👍
@DatabaseCartRepositoryQualifier | ||
class DatabaseCartRepository( | ||
private val dao: CartProductDao, | ||
) : CartRepository { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Repository 구현체 선언부에서 DatabaseCartRepositoryQualifier를 선언하는 것도 좋지만 Repository 구현체가 직접 DI 로직을 참조해야 한다는 단점이 있어요. 아키텍처 피드백은 최대한 드리지 않으려고 했지만, 지금 구조에서는 DI 라이브러리가 교체된다면 해당 Repository 구현체도 수정되어야 합니다.
Repository 구현체가 Qualifier를 의존하기보다, 의존 관계를 선언하는 선언부에서 Qualifier를 설정해두면 어떨까요?
// 기존
types {
type(CartRepository::class to InMemoryCartRepository::class)
type(CartRepository::class to DatabaseCartRepository::class)
}
// 개선 예시
// InMemoryCartRepositoryQualifier인 경우 -> CartRepository의 구현체는 InMemoryCartRepository이다
// DatabaseCartRepositoryQualifier인 경우 -> CartRepository의 구현체는 DatabaseCartRepository이다
지금 구현하신 RepositoryModule과 같이 설정하는 구조를 활용하는 것도 좋습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
제가 생각을 잘못하고 있었네요...!
DatabaseCartRepository에 애노테이션을 추가하지 않아도 되도록 변경해보겠습니다.
class MainViewModel(
private val productRepository: ProductRepository,
@InMemoryCartRepositoryQualifier
private val cartRepository: CartRepository,
) : ViewModel() {
위와 같은 구조로 변경해보겠습니다~
@InMemoryProductRepositoryQualifier | ||
class DefaultProductRepository : ProductRepository { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
사실 모든 의존 관계에서 Qualifier가 필요하지는 않습니다. 실제 다른 DI 라이브러리를 학습하다보면 아시겠지만 Qualifier 개념은 후반부에 나오는 개념이에요. (== Qualifier를 몰라도 대부분의 의존성 주입은 가능하다는 의미입니다.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
사실 Hilt와 같은 구조를 따라가고 싶어서 Qualifier를 사용했습니다. (물론 Hilt는 컴파일 타임이지만요.. 😅)
ProductRepository의 서브타입이 더 늘어났을 때를 가정해서 미리 애노테이션을 추가하였는데,
지금 당장 필요해보이지 않아서 제거 하였습니다!
|
||
object DependencyInjector { | ||
private val subTypeConverter = SubTypeConverter() | ||
private val cache = Cache() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
한 번 생성한 객체를 Cache 객체에 저장해두는 구조입니다.
지금 당장은 문제가 되지 않지만, 경우에 따라 캐싱없이 매번 새롭게 생성되어야 하는 객체들도 있을 것입니다.
바로 다음 미션에서 고민하게 되시겠네요!
fun clear() { | ||
subTypeConverter.clear() | ||
cache.clear() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
실질적으로 clear하고 있지 않습니다.
fun clear(): Cache {
return copy(cache = mutableMapOf())
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
방어적 복사만 하고 있고 실질적으로 제거는 하고 있지 않네요!
SubTypeConverter
, Cache
클래스 모두 다음과 같이 변경하였습니다.
fun clear(): SubTypeConverter {
converter.clear()
return copy(converter = mutableMapOf())
}
data class SubTypeConverter( | ||
private val converter: MutableMap<DependencyKey, KType> = mutableMapOf(), | ||
) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
현재의 DependencyInjector는 ViewModel Inject를 고려하고 설계되었지만, 사실 꼭 ViewModel에 의존할 필요는 없습니다.
모든 클래스를 커버할 수 있도록 ViewModel 의존을 걷어내신다면, 굳이 SubTypeConverter와 같은 클래스 없이도 재귀 주입을 구현할 수 있게 되실거예요.
요약: DI 라이브러리가 Android Library가 아니여도 된다.
이 코멘트는 큰 구조 변경을 요하기 때문에 참고만 해주세요! 지금 단계에서는 넘어가도 좋습니다~
import org.robolectric.RobolectricTestRunner | ||
|
||
@RunWith(RobolectricTestRunner::class) | ||
class DependencyInjectorTest { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DI 라이브러리가 제대로 작동하는지 검증하기 위한 시나리오가 많이 누락되었습니다.
현재의 테스트 코드에서는
- Qualifier에 대한 검증이 이뤄지는 것처럼 보이지 않습니다. 실제로는 가짜 데이터로 검증을 하고 있을지 몰라도, 지금보다 더 다양한 시나리오가 나올 수 있을 것 같아요. 예를 들어 저라면 다음 시나리오 등을 테스트해볼 것 같습니다.
// 성공 케이스(인터페이스 1, 구현체 1)
class ConstructorTestViewModel(
val fooDependency: FakeRepository,
)
// 실패 케이스(인터페이스 1, 구현체 2)
class ConstructorTestViewModel(
val fooDependency: FakeRepository,
)
// 성공 케이스(인터페이스 1, 구현체 2)
class ConstructorTestViewModel(
@FooQualifier
val fooDependency: FakeRepository,
)
// 성공 케이스(인터페이스 1, 구현체 2)
class ConstructorTestViewModel(
@FooQualifier
val fooDependency: FakeRepository,
@BarQualifier
val barDependency: FakeRepository,
)
- 재귀 주입도 테스트되고 있지 않습니다.
- 필드 주입에서도 Qualifier를 사용할 수 있지 않을까요?
- 개발자들이 굳이 가짜 테스트용 데이터를 확인하지 않더라도, 테스트 코드만으로 한눈에 어떤 값을 검증하려고 하는지 확인할 수 있게 개선해보면 어떨까요?
이외에도 누락된 시나리오가 있나요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
성공하는 테스트 케이스만 있다보니, 다시 봐도 프로덕션 코드를 제대로 검증하지 못하는 것 같았습니다.
말씀해주신 것처럼 조금 더 다양한 상황을 검증하는 테스트 케이스를 추가하여 추가 커밋하였습니다.
반달! 레아가 남겨주신 코멘트를 기반으로 추가 커밋하였습니다. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
부나~ 코드 잘봤습니다. 허허
테스트 커버리지가 잔뜩 높아진게 보여 인상깊었습니다.
레아가 이미 많이 봐주시기도 하셔서
일단 레아대신 피드백 반영 확인해보았고,
추가로 제가 궁금한 점이나 생각해보시면 좋을 것들 조그마하게 적어보았습니다.
코멘트 또는 코드 수정 하셧다면 확인 후 바로 어프로브 누르도록 하겠습니다!
fun 모든_장바구니_목록을_불러오면_기존_장바구니_목록을_갱신한다() { | ||
// given: 불러올 장바구니 목록이 존재한다. | ||
val expected = listOf( | ||
Product("우테코 과자", 1000, "snackimage"), | ||
Product("우테코 쥬스", 2000, "juiceimage"), | ||
Product("우테코 아이스크림", 3000, "icecreamimage"), | ||
CartProduct(0, 0L, Product("우테코 과자", 1000, "snack")), | ||
CartProduct(1, 0L, Product("우테코 쥬스", 2000, "juice")), | ||
CartProduct(2, 2L, Product("우테코 아이스크림", 3000, "icecream")), | ||
) | ||
every { cartRepository.getAllCartProducts() } returns expected | ||
coEvery { cartRepository.getAllCartProducts() } returns expected |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
coEvery
는 every
와 무엇이 다른가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
coEvery
는 이름 앞에 co
가 붙은 것처럼 코루틴(suspend) 함수를 스터빙하기 위한 용도로 사용됩니다.
suspend 함수에 every
를 사용하면 컴파일 에러가 발생하는 것을 확인할 수 있을 거예요 : )
companion object { | ||
const val DATABASE_NAME = "cart-database" | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
세심한 상수화 😎
fun List<CartProductEntity>.toDomain(): List<CartProduct> { | ||
return map { | ||
CartProduct( | ||
product = Product( | ||
name = it.name, | ||
price = it.price, | ||
imageUrl = it.imageUrl, | ||
), | ||
id = it.id, | ||
createdAt = it.createdAt, | ||
) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fun List<CartProductEntity>.toDomain(): List<CartProduct> { | |
return map { | |
CartProduct( | |
product = Product( | |
name = it.name, | |
price = it.price, | |
imageUrl = it.imageUrl, | |
), | |
id = it.id, | |
createdAt = it.createdAt, | |
) | |
} | |
} | |
fun List<CartProductEntity>.toDomain(): List<CartProduct> { | |
return map { cartProductEntity -> | |
CartProduct( | |
product = Product( | |
name = cartProductEntity.name, | |
price = cartProductEntity.price, | |
imageUrl = cartProductEntity.imageUrl, | |
), | |
id = cartProductEntity.id, | |
createdAt = cartProductEntity.createdAt, | |
) | |
} | |
} |
조금만 봐도 it
이 뭔지 유추할 수는 있지만, 지금과 같이 여러 인덴트가 들어간다면 더나은 가독성을 위해 cartProductEntity
와 같은 explicit parameter를 사용하는건 어떨까요? 물론 큰차이가 느껴지지 않는다고 부나가 느끼신다면 무시하셔도 좋습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좋은 의견이라고 생각해요~
코드를 보는데 유심히 들여다볼 정도로 복잡하지는 않지만, 명시적으로 작성해주면 좋겠다고 생각했어요.
참고로 저는 사실 대부분의 경우 아래와 같은 기준으로 explict paramter를 사용합니다.
- 하나의 파일에 코드가 길어져서 가독성이 떨어질 때
- 한 함수 내에서 depth가 길어질 때
class CartViewModel( | ||
private val cartRepository: CartRepository, | ||
@DatabaseCartRepositoryQualifier val cartRepository: CartRepository, | ||
) : ViewModel() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
뷰모델은 레포지토리를 통해 그저 데이터를 관리하는데에만 관심사가 있습니다.
다르게 말하면 우리가 레포지토리 패턴을 사용한다는 가정하에 레포지토리의 데이터 소스로 Local이 오냐 Remote가 오냐는 레포지토리 내부 사정이라고 생각하는데 부나의 어노테이션은 데이터베이스, 즉 데이터 소스의 성격을 표시하고 있습니다.
부나가 생각하시기에 어떤가요?? 상관없을 것 같다면 반영하시지 않으셔도 좋습니다. 다만 부나의 의견은 궁금하니 코멘트 남겨주세요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저도 반달과 동일한 생각이예요!
하지만 이 외에는 별다른 아이디어가 떠오르지 않아 위와 같은 구조로 설계하였습니다.
제가 아는 선에서 사실 Hilt도 위와 같은 형식으로 애노테이션을 지정해주고 있는 것으로 알고 있어요!
저도 이 부분에 영향을 많이 받아서 위처럼 작성하였습니다. 😅
참고 자료를 위해 링크를 함께 첨부해드려요 : )
Hilt 식별자
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
의견 잘보았습니다. 부나!
4단계도 똑같이 홧팅입니다!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
안녕하세요 반달!
2, 3단계 미션 완료하여 리뷰요청드립니다.
요구사항 체크리스트는 다음과 같습니다.
2단계
3단계
지금 당장 하지 않은 로직
한 번 생성한 객체를 Cache 객체에 저장해두는 구조입니다.
지금 당장은 문제가 되지 않지만, 경우에 따라 캐싱없이 매번 새롭게 생성되어야 하는 객체들도 있을 것입니다.
그런 문제는 이후에
@Singleton
과 같은 애노테이션을 통해 해결해보려고 합니다.리뷰 잘 부탁드립니다 : )