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

[베리] 2, 3단계 자동 DI 미션 제출합니다 #46

Merged
merged 23 commits into from
Sep 18, 2023

Conversation

whk06061
Copy link

안녕하세요 수달 ~! 그동안 잘 지내셨나용 ㅎㅎ🦦
2,3 단계 자동 DI 미션 제출합니다!
시간이 부족해서 필수 요구사항만 만족했습니다 😅 선택 요구사항은 차차 채워나가겠습니다!
그리고 기존에는 Repository 인스턴스를 Module 클래스 안에서 생성한 후, 해당 Module 리스트를 Injector 가 가지고 있는 형태였습니다.
그러나 기존 구조로는 Recursive DI 를 구현하는데에 한계가 생겨서 구조를 조금 수정하였습니다!
현재는 Application 에서 Repository 인스턴스를 생성하여 Container Object 에 저장하여 Injector 가 이 Container 에 접근하여 인스턴스를 가져오는 구조로 수정하였습니다!

2단계 요구 사항 중 Recursive DI 부분 제 코드가... 뭔가 자동 Recursive DI 가 아니라...수동의 느낌이...물씬 나는 점 피드백 부탁드립니다! ㅠ

추가로 궁금한 점 있으시면 편하게 질문주세요!🤗

android-di

2 단계 - Annotation

기능 요구 사항

필드 주입

  • ViewModel 내 필드 주입을 구현한다.

Annotation

  • 의존성 주입이 필요한 필드와 그렇지 않은 필드를 구분할 수 없다.
    • Annotation을 붙여서 필요한 요소에만 의존성을 주입한다.
    • 내가 만든 의존성 라이브러리가 제대로 작동하는지 테스트 코드를 작성한다.

Recursive DI

  • CartRepository가 다음과 같이 DAO 객체를 참조하도록 변경한다.
  • CartProductViewHolder의 bind 함수에 다음 구문을 추가하여 뷰에서도 날짜 정보를 확인할 수 있도록 한다.

선택 요구 사항

  • 현재는 장바구니 아이템 삭제 버튼을 누르면 RecyclerView의 position에 해당하는 상품이 지워진다.
    • 상품의 position과 CartRepository::deleteCartProduct의 id가 동일한 값임을 보장할 수 없다는 문제를 해결한다.
  • 뷰에서 CartProductEntity를 직접 참조하지 않는다.

프로그래밍 요구 사항

  • 사전에 주어진 테스트 코드가 모두 성공해야 한다.

3 단계 - Qualifier

기능 요구 사항

Qualifier

  • 하나의 인터페이스의 여러 구현체가 DI 컨테이너에 등록된 경우, 어떤 의존성을 가져와야 할지 알 수 없다.
    • 상황에 따라 개발자가 Room DB 의존성을 주입받을지, In-Memory 의존성을 주입받을지 선택할 수 있다.

모듈 분리

  • 내가 만든 DI 라이브러리를 모듈로 분리한다.

선택 요구 사항

  • DSL을 활용한다.
  • 내가 만든 DI 라이브러리를 배포하고 적용한다.

@whk06061 whk06061 self-assigned this Sep 13, 2023
Copy link

@otter66 otter66 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 18 to 31
private fun initContainer() {
DefaultContainer.addInstance(
DefaultProductRepository()
)
DefaultContainer.addInstance(
ShoppingDatabase.getDatabase(applicationContext).cartProductDao()
)
DefaultContainer.addInstance(
InMemoryCartRepository()
)
DefaultContainer.addInstance(
Injector.inject<RoomDBCartRepository>()
)
}
Copy link

Choose a reason for hiding this comment

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

이전에 다른 크루에게 달린 리뷰를 참고해보겠습니다!

컴포넌트로 등록하려는 대상 클래스에 추가한 애너테이션을 활용하면 이와 같은 코드를 없애고 라이브러리를 더 사용자 친화적으로 만들 수 있을 거예요. 아래의 글이 도움 될 거예요.
https://www.baeldung.com/reflections-library

#28 (comment)

binding.item = product
binding.tvCartProductCreatedAt.text = dateFormatter.formatDate(product.createdAt)
// TODO: Step2 - dateFormatter를 활용하여 상품이 담긴 날짜와 시간을 출력하도록 변경
Copy link

Choose a reason for hiding this comment

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

TODO 주석은 이만 보내줘도 괜찮을 것 같아요!! 안녕~👋🏼

// Reflection
implementation(kotlin("reflect"))

testImplementation("junit:junit:4.13.2")
Copy link

Choose a reason for hiding this comment

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

junit4를 사용하신 이유가 있을까요?

Copy link
Author

Choose a reason for hiding this comment

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

지금까지 junit4 를 써와서 익숙한 라이브러리를 사용하였습니다! 🤗

// Inject 어노테이션 붙은 파라미터들만 들어옴
val dependencies: MutableMap<KParameter, Any> = mutableMapOf()
parameters.forEach { parameter ->
val annotation: Annotation? = parameter.annotations.firstOrNull { it != Inject() }
Copy link

Choose a reason for hiding this comment

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

문맥상 Inject 어노테이션이 있는지 확인해야할 것 같아요.
이 경우에는 firstOrNull은 적절하지 않습니다.
더 적절한 api를 활용해주세요!

val annotation: Annotation? = parameter.annotations.firstOrNull { it != Inject() }
val annotationType = AnnotationType(annotation, parameter.type.jvmErasure)
dependencies[parameter] =
container.getInstance(annotationType) ?: throw NoSuchElementException()
Copy link

Choose a reason for hiding this comment

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

throw Exception을 할 때 메시지가 없다면 오류 발생시 원인을 찾기 어려울 것 같습니다.

}

@Test
fun `의존성 모듈이 모두 제공될 때 자동 DI 가 성공하는지 테스트`() {
Copy link

@otter66 otter66 Sep 14, 2023

Choose a reason for hiding this comment

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

테스트는 명세서의 역할도 있습니다!
때문에 테스트명은 기능 명세서처럼 작성하면 좋을 것 같습니다.
"필요한 의존성에 대한 모듈이 제공되면 의존성을 주입한다"와 같이요!

Copy link

@otter66 otter66 left a comment

Choose a reason for hiding this comment

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

다음 리뷰 요청때까지 더 많은 미션 요구사항을 만족시킨 상태라면 더 더 더 좋을 것 같아요!
어렵겠지만 조금만 더 화이팅해보자고요!
베리의 다음 리뷰요청 기대하면서 저도 열심히 해보겠습니다!

@whk06061
Copy link
Author

수달~!~! 드디어 리뷰 요청 보냅니당..ㅎㅎ
이전 코드 구조가 Recursive 를 제대로 사용하지 않고, 수동 주입처럼 구현이 되어서 코드를 대폭 수정해보았습니다!
변경 사항은 다음과 같습니다!

  • 의존성 주입이 필요한 곳에 @Inject 키워드를 붙여주었고, 만약 하나의 인터페이스에 대해 구현체가 두 개인 경우에만 @qualifier 어노테이션으로 적절한 구현체를 찾아올 수 있도록 구현하였습니다!
  • 필요한 의존성 구현체들은 Container 의 함수로 불러오도록 구현하였습니다. 여기에서도 하나의 인터페이스에 구현체가 두 개인 경우에만 @qualifier 어노테이션을 붙여주었습니다.
  • 만약 Container 의 의존성 구현체에 또 의존성이 있는 경우, Injector 에서 Recursive 함수를 통해 불러오도록 구현했습니다!

Copy link

@otter66 otter66 left a comment

Choose a reason for hiding this comment

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

베리 step2, step3 필수 요구사항을 모두 잘 만족시키신 것 같아요! 고생 많으셨습니다!!
추가로 고민해봤으면 하는 부분에 대해 코멘트 남겨두었습니다.
step4 마감일이 많이 남지 않았으니 읽어만 보시고, step4 요구사항 구현을 우선시하면 될 것 같습니다!
화이팅입니다!

import woowacourse.shopping.model.CartProduct
import woowacourse.shopping.model.Product

class RoomDBCartRepository @Inject constructor(private val dao: CartProductDao) :
Copy link

Choose a reason for hiding this comment

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

constructor에 Inject를 달면 사용할 때 편할 것 같네요! 줍줍해갑니다!

@@ -14,10 +15,12 @@ import kotlin.reflect.full.memberFunctions
import kotlin.reflect.full.staticFunctions
import kotlin.reflect.jvm.jvmErasure

annotation class AA
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 +16 to +26
fun getDatabase(context: Context): ShoppingDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context,
ShoppingDatabase::class.java,
databaseName
).build()
INSTANCE = instance
instance
}
}
Copy link

Choose a reason for hiding this comment

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

인스턴스화에 사용한 context가 역할을 다하고 종료되어 사라졌을 때 문제가 발생할 수 있을 것 같아요.
이는 step4와도 관련이 있을 것 같네요!

Comment on lines +36 to +43
@Test
fun `인터페이스가 아닌 클래스를 생성자로 주입받는 경우 생성자에 @Inject 키워드만 붙이면 된다`() {
// given
class FakeViewModel @Inject constructor(val fakeRepository: FakeRepository)

// when
Injector.inject<FakeViewModel>()
}
Copy link

Choose a reason for hiding this comment

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

than이 없어요😱
생성이 잘 됐다는 것을 확인하는게 애매할 수 있을 것 같네요.
저의 경우에는 than에서 FakeViewModelfakeRepositorynull이 아닌지, 원하는 구현체 타입이 맞는지 등을 확인하였습니다.

Copy link

Choose a reason for hiding this comment

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

Injector가 유능하게도 혼자서 많은 일을 하고 있는 것 같아요.
Injector의 일을 덜어줄 객체가 있다면 좋을 것 같아요!

Comment on lines +50 to 52
coEvery {
cartRepository.addCartProduct(product)
} answers { nothing }
Copy link

Choose a reason for hiding this comment

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

mock 객체를 지원하는 라이브러리를 사용하지 않고, fake 객체를 사용해 테스트하는 방법도 있습니다!

@otter66 otter66 merged commit 8d0bdfd into woowacourse:whk06061 Sep 18, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants