Skip to content

[쇼핑 주문 3, 4단계] 공백 미션 제출합니다. #121

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

Merged
merged 69 commits into from
Jun 10, 2025

Conversation

junseo511
Copy link

@junseo511 junseo511 commented Jun 2, 2025

< 두루의 소중한 시간을 내어 리뷰해주셔서 감사합니다 🙇‍♀️ >

셀프 체크리스트

  • 프로그램이 정상적으로 작동하는가?
  • 모든 테스트가 통과하는가?
  • 이전에 받은 피드백을 모두 반영하였는가?
  • 코딩 스타일 가이드를 준수하였는가?
    • IDE 코드 자동 정렬을 적용하였는가?
    • 린트 검사를 통과하였는가?
  • 프로그래밍 요구 사항을 준수하였는가?
  • README.md에 기능 목록을 정리하고 명확히 기술하였는가?

어떤 부분에 집중하여 리뷰해야 할까요?

안녕하세요, 두루!
두루와 마지막 미션을 함께 진행하고 있어 즐겁습니다 😄

이전 PR에서 나눈 이야기도 코드 작성에 도움이 되었어요 👍
해당 페이지에서 남은 코멘트에 대한 답변을 드렸습니다!

꽤나 많은 코드가 추가되었는데 항상 열심히 확인해주셔서 감사합니다 😅

이번 미션에서는 다음과 같은 사항에 집중해 작업했습니다.

  • 3단계 미션 구현사항에 따라 콜백 지옥을 정리했습니다.
  • 각 구현 내용이 다른 쿠폰을 추상화하는 방법에 대해 깊게 고민했습니다.
    • 인터페이스로 Coupon 이라는 객체를 두고 사용 여부와 적용을 진행했습니다.
  • 장바구니 상품, 추천 상품 등 서로 동기화되어야 하는 정보가 꽤 있어서 UI 핸들링을 data class를 활용해 진행했습니다.
    해당 코멘트에서 진행한 것처럼 nullable한 데이터를 줄이고 싶은데 어떤 방법을 사용해야 할지 고민중입니다 🤔

질문사항

  • 이전 PR에서 남겨드렸듯 비즈니스 로직을 서버가 해결하지 못하는 상황에서 UseCase 로 꽤 득을 보았습니다.
    하지만, 코루틴을 적용하고 suspend fun으로 바꾸는 과정에서 로직의 길이가 크게 줄었는데,
    이전에 비해 UseCase 의 효과가 감소했다는 느낌을 받았습니다.

    수업때 다음과 같은 추상화나 책임 분리에 대한 이야기를 들었습니다.
    100번 중 한 번의 유용한 사용을 위해 우리는 99번 더 만드는 것이다.
    이에 대해 두루의 의견이 궁금합니다!

스크린샷

Screen_recording_20250606_220002.mp4

Copy link

coderabbitai bot commented Jun 2, 2025

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Note

Reviews paused

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Summary by CodeRabbit

  • Documentation
    • README 파일에 개발 3, 4단계의 상세 기능 요구사항이 추가되었습니다.
    • 코루틴 기반 비동기 처리, 장바구니에서 주문 및 고정 배송비 적용, 결제 화면에서 쿠폰 조회 및 적용(1회 제한) 관련 내용이 포함되었습니다.
    • 주문 완료 시 동작, 쿠폰 유형 및 조건, 구현 체크리스트가 명확히 안내되었습니다.

Walkthrough

README.md 파일이 업데이트되어 개발 3, 4단계의 상세 기능 요구사항이 추가되었습니다. 주요 변경 내용에는 Kotlin Coroutines(Flow 제외)을 활용한 비동기 요청 처리, 장바구니에서 최종 주문 시 3,000원 고정 배송비 적용, 결제 화면에서 쿠폰 조회 및 1회 1매 쿠폰 적용 기능 명시, 결제 버튼 클릭 시 실제 결제 구현 없이 주문 완료 처리 및 토스트 메시지와 상품 목록 화면 이동 등이 포함됩니다. 주문 완료 시 장바구니는 비워지나 쿠폰은 그대로 유지되며, 4종류의 쿠폰 상세 조건도 문서에 추가되었습니다. 마지막으로, 각 단계별 구현 체크리스트가 추가되었습니다.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

‼️ IMPORTANT
Auto-reply has been disabled for this repository in the CodeRabbit settings. The CodeRabbit bot will not respond to your replies unless it is explicitly tagged.

  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
README.md (1)

29-36: 마크다운 중첩 리스트 들여쓰기 일관성 유지 필요
중첩된 리스트 항목이 4칸 들여쓰기( - )로 작성되어 있는데, markdownlint(MD007)에서는 2칸 들여쓰기를 권장합니다. 예를 들어 다음과 같이 변경해주세요.

-    - 단, Flow를 사용하진 않는다.
+  - 단, Flow를 사용하진 않는다.

-    - 배송비는 기본 3,000원이다.
+  - 배송비는 기본 3,000원이다.

-    - 쿠폰은 1개만 적용 가능하다.
+  - 쿠폰은 1개만 적용 가능하다.

-    - 결제하기 버튼을 누르면 바로 최종 주문이 완료된다.
+  - 결제하기 버튼을 누르면 바로 최종 주문이 완료된다.

-    - 최종 주문이 완료되면 상품 목록으로 이동과 함께 주문 완료 토스트 메시지를 노출한다.
+  - 최종 주문이 완료되면 상품 목록으로 이동과 함께 주문 완료 토스트 메시지를 노출한다.
🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

29-29: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


31-31: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


33-33: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


35-35: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


36-36: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Knowledge Base: Disabled due to Reviews > Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between ef367f9 and 01d94bc.

📒 Files selected for processing (1)
  • README.md (2 hunks)
🧰 Additional context used
🪛 markdownlint-cli2 (0.17.2)
README.md

29-29: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


31-31: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


33-33: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


35-35: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


36-36: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

🔇 Additional comments (1)
README.md (1)

84-93: 체크리스트 상태가 구현 현황을 반영하는지 확인해주세요
3,4단계 구현 기능에 대한 체크박스가 현재 대부분 미체크([ ])로 남아 있습니다. 실제 구현이 완료된 항목만 [x]로 표시하고, 아직 진행 중인 항목은 그대로 두어 문서가 현재 구현 상태를 정확히 반영하도록 업데이트해 주세요.

@junseo511
Copy link
Author

@coderabbitai pause

Copy link

coderabbitai bot commented Jun 2, 2025

✅ Actions performed

Reviews paused.

@Gyuil-Hwnag Gyuil-Hwnag self-requested a review June 7, 2025 01:53
Copy link
Member

@Gyuil-Hwnag Gyuil-Hwnag left a comment

Choose a reason for hiding this comment

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

공백 안녕하세요~
해당 미션 구현 고생 많으셨습니다!
미션 구현을 잘 해주셨습니다 💯

이번 코루틴 미션 적용 많이 어려우셨을텐데, 적용 너무 잘해주셨습니다👍
추가로 고민해보시면 좋을만한 내용들 코멘트를 달았으니 확인해주시면 감사하겠습니다~

궁금한 점이나 논의하고 싶은 내용이 있으면 언제든 DM 주세요! 🙏
화이팅입니다💪

Comment on lines +24 to +26
## 🧱 기능 요구 사항 (3,4단계)

### 📕 기획 명세
Copy link
Member

Choose a reason for hiding this comment

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

기획명세에 대한 정의와 README 작성 너무 좋네요~
추가적으로 한가지팁을 드려보자면, 단계별 나왔던 키워드를 살짝 남겨둔다면 추후에 해당 내용을 보았을 때 아~ 이때 이런 키워드가 나왔었지 이렇게 좀 더 기억에 남을 수도 있겠군요💪

Copy link
Author

@junseo511 junseo511 Jun 7, 2025

Choose a reason for hiding this comment

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

기획명세에 대한 정의와 README 작성 너무 좋네요~ 추가적으로 한가지팁을 드려보자면, 단계별 나왔던 키워드를 살짝 남겨둔다면 추후에 해당 내용을 보았을 때 아~ 이때 이런 키워드가 나왔었지 이렇게 좀 더 기억에 남을 수도 있겠군요💪

한 번 열심히 리마인드 해보겠습니다~~~ 👍
b173040

Comment on lines -42 to +39
}
fun 검색_기록을_저장하고_조회한다() =
runTest {
// given
Copy link
Member

Choose a reason for hiding this comment

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

runTest를 활용한 코루틴 테스트 아주 좋네요~
runTest 로 하게 된다면, 일발적으로는 어떤 디스패처를 통해서 테스트를 하게 되는 것일까요?!
(이 부분과 직접적으로 연관된 내용을 아닐 것 같지만, 한번 알아둔다면 도움이 될 것 같아서 남겨두었습니다😊
사실 연관이 있는 부분은 Ui테스트 부분이겠군요)

이전에 마이그레이션 하다가 withContext 또는 CoroutineScope.launch(Dispatcher.Main)등 직접 메인 디스패처로 정의를 하는 로직이 있은 곳에서는 현재처럼 runTest 내의 모든 suspend 함수를 넣었을 때, 재대로 테스트가 동작을 통과를 못하는 경우가 있었어서, 기억이 났습니다~

Copy link
Author

@junseo511 junseo511 Jun 7, 2025

Choose a reason for hiding this comment

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

runTest를 활용한 코루틴 테스트 아주 좋네요~ runTest 로 하게 된다면, 일발적으로는 어떤 디스패처를 통해서 테스트를 하게 되는 것일까요?! (이 부분과 직접적으로 연관된 내용을 아닐 것 같지만, 한번 알아둔다면 도움이 될 것 같아서 남겨두었습니다😊 사실 연관이 있는 부분은 Ui테스트 부분이겠군요)

공식문서는 다음과 같이 명시하고 있습니다.

The test is run on a single thread, unless other CoroutineDispatcher are used for child coroutines.

테스트만을 위한 싱글 스레드에서 작업을 진행하나보네요!
내부 코드도 살펴보면

public fun runTest(
    context: CoroutineContext = EmptyCoroutineContext,
    timeout: Duration = DEFAULT_TIMEOUT.getOrThrow(),
    testBody: suspend TestScope.() -> Unit
): TestResult {
    check(context[RunningInRunTest] == null) {
        "Calls to `runTest` can't be nested. Please read the docs on `TestResult` for details."
    }
    return TestScope(context + RunningInRunTest).runTest(timeout, testBody)
}

위와 같이 context도 EmptyCoroutineContext 로 진행이 되고 있습니다
함수가 테스트만을 위한 공간을 만들어주는걸까요 🤔

Copy link
Member

Choose a reason for hiding this comment

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

함수가 테스트만을 위한 공간을 만들어주는걸까요 🤔

혹시 이 내용이 어떤 내용인지 좀 더 설명이 가능할까요~?
사실 이 질문은 advanceUntilIdle()등의 메서드가 있어서, 이걸 한번 알아두고자 시작했던 질문이었습니다

Copy link
Author

@junseo511 junseo511 Jun 9, 2025

Choose a reason for hiding this comment

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

혹시 이 내용이 어떤 내용인지 좀 더 설명이 가능할까요~? 사실 이 질문은 advanceUntilIdle()등의 메서드가 있어서, 이걸 한번 알아두고자 시작했던 질문이었습니다

해당 내용을 위한 공식문서가 따로 있었네요!!
테스트 과정에서 말씀하신 함수 활용을 통해 코루틴 실행을 보장할 수 있겠어요 👍
바로 진행해보도록 하겠습니다 :)
3561772

Copy link
Member

Choose a reason for hiding this comment

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

하나를 말씀드리면, 열에 대한 내용을 알아오시는군요👍

Comment on lines +14 to +25
class CoroutinesTestExtension(
private val dispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : BeforeEachCallback,
AfterEachCallback {
override fun beforeEach(context: ExtensionContext?) {
Dispatchers.setMain(dispatcher)
}

override fun afterEach(context: ExtensionContext?) {
Dispatchers.resetMain()
}
}
Copy link
Member

Choose a reason for hiding this comment

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

CoroutineTestExtension의 활용 정말 좋습니다~

Comment on lines +10 to +11
DatabaseInjection.init(this)
PreferenceInjection.init(this)
Copy link
Member

Choose a reason for hiding this comment

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

오호~ 저번 미션에서의 코멘트의 injection 이라는 네이밍이 마음에 드셨군요😊
반영 너무 좋습니다!

Copy link
Author

Choose a reason for hiding this comment

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

오호~ 저번 미션에서의 코멘트의 injection 이라는 네이밍이 마음에 드셨군요😊 반영 너무 좋습니다!

앞으로는 Injection 을 애용할 것 같아요 🤣

Comment on lines +17 to +20
suspend fun getCartItems(
@Query("page") page: Int,
@Query("size") size: Int,
): Call<CartItemsResponse>
): CartItemsResponse
Copy link
Member

Choose a reason for hiding this comment

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

이 부분들이 모두 Coroutine에 맞도록 변경이 되었군요~
이 부분과 연관된 내용은 아니지만, 올해초에 Rx 마이그레이션을 하면서 이러한 수정 작업을 많이 거쳤었는데 그때 생각이 나는군요
이번 경험이 추후 Rx or Thread 에서 코루틴으로 마이그레이션을 하는데에 도움이 되었으면 좋겠군요!

Copy link
Author

@junseo511 junseo511 Jun 7, 2025

Choose a reason for hiding this comment

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

이번 경험이 추후 Rx or Thread 에서 코루틴으로 마이그레이션을 하는데에 도움이 되었으면 좋겠군요!

2025년도에도 이런 마이그레이션이 필요한 곳이 많다니,,, 미래가 두려운데요,,?!

Copy link
Member

Choose a reason for hiding this comment

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

되어 있는곳들도 많겠지만, 오래된 서비스일수록 엮여있는 모든 로직들을 파악하고 신규 피처도 치면서 작업을 하기에는 무리가 있다보니... 은근 남아있는 곳들도 있었어요~😅

Comment on lines +26 to +36
private fun getUsername(): String =
authSharedPreference.getAuthUsername()
?: BuildConfig.DEFAULT_USERNAME.apply {
authSharedPreference.putAuthId(this)
}

private fun getPassword(): String =
authSharedPreference.getAuthPassword()
?: BuildConfig.DEFAULT_PASSWORD.apply {
authSharedPreference.putAuthPassword(this)
}
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
Author

Choose a reason for hiding this comment

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

오호 직접 다이렉트로 넣는 내용에서 함수를 호출하는 방식으로 변경이 되었군요~ 개인적으로는 이전에 직접 넣는 방식도 괜찮았었는데, 이렇게 변경이 되게 되었던 생각의 흐름이 궁금합니다!

기존에 해당 작업은 NetworkInjection 이라는 곳에서 이루어지고 있었습니다!
하지만 Injection이라는 객체는 말 그대로 주입만 해주는 곳이라고 생각되어,
네트워크 관련 의존성의 주입만 진행하고 로컬 저장소의 정보를 가져오는 곳이 아니라고 생각되었습니다!

하지만 AuthInterceptor 는 충분히 Auth에 필요한 정보들을 직접 가져오는 역할까지는 하는게 괜찮다고 생각되어,
의존성을 주입받고 해당 작업을 인터셉터 내에서 진행하도록 하는게 맞다고 판단했습니다 :)

Copy link
Member

Choose a reason for hiding this comment

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

오호~ 그렇군요!
그렇다면, 아주 좋습니다👍
만약 재 로그인등에 대해서도 Preference 의 싱크를 잘 맞춰주면 문제가 없겠군요

Comment on lines +6 to +14
@Serializable
data class SortResponse(
@SerialName("empty")
val empty: Boolean,
@SerialName("sorted")
val sorted: Boolean,
@SerialName("unsorted")
val unsorted: Boolean,
)
Copy link
Member

Choose a reason for hiding this comment

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

중복적으로 사용하는 내용들에 대한 분리 아주 좋습니다~👍

Comment on lines 20 to 28
withContext(Dispatchers.IO) {
runCatching {
api.getCartItems(page, size)
}.mapCatching { response ->
val items = response.content.map { it.toDomain() }
val pageInfo = Page(page, response.first, response.last)
Products(items, pageInfo)
}
}
Copy link
Member

Choose a reason for hiding this comment

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

오호~ withContext를 사용해주셨군요~
withContext는 어떤 동작을 하며, withContext 를 사용해주신 이유가 있었을까요?!🤔

Copy link
Author

Choose a reason for hiding this comment

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

오호~ withContext를 사용해주셨군요~ withContext는 어떤 동작을 하며, withContext 를 사용해주신 이유가 있었을까요?!🤔

withContext 는 코루틴 launch 내에서 잠시 다른 Dispatcher로 이동해서 작업하다가 돌아오는 일을 하도록 만듭니다!
네트워크 통신 등은 UI에 영향을 미치지 않는 선인 IO에서 이루어지는게 더 적절하다고 생각되어 진행했습니다 :)

Copy link
Member

Choose a reason for hiding this comment

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

흠~ withContext 가 반드시 있어야 할까요..?🤔

Copy link
Author

Choose a reason for hiding this comment

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

흠~ withContext 가 반드시 있어야 할까요..?🤔

withContext를 해당 방식으로 사용하면 테스트의 유연성이 떨어지는 문제가 있다네요 ㅠ
ViewModel에서 디스패처를 골라주자니 setValue 등에서 오류가 발생하는데,
강의 시간에 배운 것처럼 네트워크 관련 로직을 IO에서 실행시킬 수 있는 좋은 다른 방법이 뭐가 있을까요? 🤔
우선 withContext 함수들은 제거를 진행하도록 해보겠습니다!
5ddd01f

Copy link
Member

@Gyuil-Hwnag Gyuil-Hwnag Jun 9, 2025

Choose a reason for hiding this comment

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

그렇군요~
사실 이 코멘트에 대한 키포인트는 아래의 내용이었습니다!
Retrofit + Coroutine 을 사용할 때 반드시 Dispatcher.IO 로 지정을 해줘야 할까요? Retrofit 내부에서 알아서 해줄텐데 말이지요

Copy link
Author

@junseo511 junseo511 Jun 10, 2025

Choose a reason for hiding this comment

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

그렇군요~ 사실 이 코멘트에 대한 키포인트는 아래의 내용이었습니다! Retrofit + Coroutine 을 사용할 때 반드시 Dispatcher.IO 로 지정을 해줘야 할까요? Retrofit 내부에서 알아서 해줄텐데 말이지요

Retrofit이 굉장히 잘 만들어져 있다는 라이브러리임을 들었는데,
이렇게까지 스레드 풀까지 관리해줄 줄은 몰랐네요 😅
Dispatcher.IO 등을 지정하지 않고 사용하는게 맞다고 하네요

Retrofit2는 어떻게 동작하는가 — 2. IO Dispatcher는 필요한가
해당 글에서는 Retrofit을 실행하면 기본적으로 MainThreadExecutor로 실행이 되나,
직접 UI 스레드에서 굴러가는건 또 아니라고 하고 있습니다 🤔

Retrofit2는 어떻게 동작하는가 — 3. OkHttp3의 스레드 관리
이 글에서는 OkHttp 기반으로 굴러가는 Retrofit은
OkHttp의 Dispatcher가 준비해주는 스레드 풀 내부에서 작동하도록 만들어준다고 되어 있어요 🤔

열심히 읽고 돌아와봤지만
뭔가 나름의 최적화를 해준 것 같은데 아직 와닿지가 않네요 🥲
조금 더 쉬운 레퍼런스가 있을까요...?

Comment on lines +5 to +6
@JvmInline
value class Coupons(
Copy link
Member

Choose a reason for hiding this comment

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

value class를 활용해주셨군요~
해당 class는 어떤 동작을 하고, @JvmInline 어노테이션은 어떤 동작을 하게 될까요~?

Copy link
Author

@junseo511 junseo511 Jun 7, 2025

Choose a reason for hiding this comment

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

value class를 활용해주셨군요~ 해당 class는 어떤 동작을 하고, @JvmInline 어노테이션은 어떤 동작을 하게 될까요~?

inline 은 코드 또는 데이터가 별도의 위치에서 호출되지 않고 더 큰 코드 블록 내의 적절한 위치에 직접 삽입되는 컴퓨팅 용어를 뜻한다고 합니다.
value class는 이러한 특성을 잘 나타내 실제로 인스턴스를 매번 생성하지 않고 일반 변수형처럼 작동하게 됩니다!
@JvmInline 은 이러한 역할을 직접적으로 명시해주는 일을 하겠네요 :)

내부 도큐먼트 주석에는 다음과 같이 나옵니다.
Specifies that given value class is inline class.

Copy link
Member

Choose a reason for hiding this comment

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

크흐~ 너무 깔끔하게 질문 잘 정리해주셨군요👍

@Gyuil-Hwnag Gyuil-Hwnag self-requested a review June 8, 2025 08:23
Copy link
Member

@Gyuil-Hwnag Gyuil-Hwnag left a comment

Choose a reason for hiding this comment

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

공백 안녕하세요~
해당 미션 구현 고생 많으셨습니다!
리뷰 반영하면서 구현한 내용 수정 너무 잘 해주셨습니다 💯

마지막 미션인 만큼 공백과 의견들을 나눠보면 좋을 내용들이 몇개 있어서, 남겨두었습니다!
고민해보시면 좋을만한 내용들 코멘트를 달았으니 확인해주시면 감사하겠습니다.

궁금한 점이나 논의하고 싶은 내용이 있으면 언제든 DM 주세요! 🙏
마지막까지 화이팅입니다💪

Comment on lines +36 to +39
<activity
android:name=".ui.payment.PaymentActivity"
android:exported="false"
android:parentActivityName=".ui.catalog.CatalogActivity" />
Copy link
Member

Choose a reason for hiding this comment

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

parentActivityName에 대한 설정을 해주셨네요👍

Comment on lines +68 to +79
private fun String.toDiscountType(): CouponDiscountType? =
when (this) {
"fixed" -> FIXED
"buyXgetY" -> BUY_X_GET_Y
"freeShipping" -> FREE_SHIPPING
"percentage" -> PERCENTAGE
else -> {
Log.e("[CouponResponse]", "유효하지 않은 쿠폰 타입이 존재합니다!")
null
}
}
}
Copy link
Member

Choose a reason for hiding this comment

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

enum class 의 변수등을 활용 하는 방법도 생각을 해봤는데, 공백의 의견은 어떠신가요~?

Copy link
Author

@junseo511 junseo511 Jun 9, 2025

Choose a reason for hiding this comment

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

enum class 의 변수등을 활용 하는 방법도 생각을 해봤는데, 공백의 의견은 어떠신가요~?

저도 처음에는 두루의 말씀처럼 enum class 로 분류를 하였으나,
실제 코드 텍스트값을 domain에서 가지고 있는 것은 도메인이 서버 data에 의존하는 것이라는 생각을 했습니다!
따라서 아래 커밋에서 원래 enum에서 관리하던 로직을 분리하였는데요,
refactor: 쿠폰 타입 변환 로직 이동

data 레이어에 또 다른 enum class를 두고 매핑하는 과정을 거치자니,
똑같이 생긴 불필요한 모델이 늘어나는 느낌을 받았습니다.
결국에는 매핑을 위한 함수도 하나 여전히 필요하게 되는 것이구요 🤔
똑같이 생긴 모델을 실수로 다르게 만들었을 경우에 대한 위험부담이 더 크다는 생각이 들었습니다!

그래서 data에 있는 Mapper 함수에 서버 코드를 파싱하는 책임을 위임하고,
만약 문제가 생겼을 경우에 파일명과 에러 로그를 남겨두는 방식을 채택했는데
두루는 어떤 방식이 더 좋으신지 의견이 궁금합니다!

Copy link
Member

@Gyuil-Hwnag Gyuil-Hwnag Jun 9, 2025

Choose a reason for hiding this comment

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

data 레이어에 또 다른 enum class를 두고 매핑하는 과정을 거치자니,
똑같이 생긴 불필요한 모델이 늘어나는 느낌을 받았습니다.
결국에는 매핑을 위한 함수도 하나 여전히 필요하게 되는 것이구요 🤔

오호! 이 내용 너무 좋네요👍

그래서 data에 있는 Mapper 함수에 서버 코드를 파싱하는 책임을 위임하고,
만약 문제가 생겼을 경우에 파일명과 에러 로그를 남겨두는 방식을 채택했는데

사실 저도 개발을 한다면, 이와 같은 방식을 채택할 것 같아요~

도메인에서는 해당 enum 값을 사용할 것이고, string 값을 사용하는 경우는 많이 없어서요!
그치만, 얼마전에 개발을 하면서 있었던 내용이 있었어요.
이벤트 로그로 해당 String 값을 남기는 경우가 있어서, 이러한 경우에는 enum 의 변수값을 설정을 해두는 방법도 고려를 해볼 필요가 있겠군요

Copy link
Author

Choose a reason for hiding this comment

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

이벤트 로그로 해당 String 값을 남기는 경우가 있어서, 이러한 경우에는 enum 의 변수값을 설정을 해두는 방법도 고려를 해볼 필요가 있겠군요

해당 String을 그대로 사용해야 할 일이 있다면 필요할 수도 있겠어요 😅

Copy link
Member

@Gyuil-Hwnag Gyuil-Hwnag Jun 10, 2025

Choose a reason for hiding this comment

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

왜 항상 개발은 구상한대로 안흘러가는지...ㅎㅎ 로그에 대한 요구사항이 오는 시기는 회사마다 다를 수 있는데, 막판에 요청이 온다면 구조를 바꿔야 하는 경우도 종종 있었어요

Comment on lines 9 to 16
suspend operator fun invoke(): Result<Coupons> {
val coupons =
repository.fetchAllCoupons().getOrElse {
return Result.failure(Throwable("[GetCouponsUseCase] 쿠폰 목록 불러오기 오류", it))
}

return Result.success(Coupons(coupons))
}
Copy link
Member

Choose a reason for hiding this comment

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

개인적으로 이제는 Coroutine으로 넘어오게 되면서, Result를 걷어내는 방식을 생각해보았는데, 공백의 의견은 어떠신가요~?
Coroutine 에서는 아래와 같은 방법으로 Exception 에 대해서 지정을 해주는 방식도 있습니다!
(힌트로 주고, 직접 찾아가도록 해볼 걸.. 이라는 생각이 들었지만 한번 바로 의견을 들어보는 것도 좋을 것 같아서, 이렇게 남겨두었습니다!)

        // 기존 방식
        viewModelScope.launch {
            getCouponsUseCase()
                .onSuccess {
                    updateUiState { current ->
                        current.copy(
                            coupons = it.filterAvailableCoupons(products, nowDateTime),
                            price = current.coupons.applyCoupon(products, nowDateTime),
                        )
                    }
                }.onFailure {
                    updateUiState { current -> current.copy(connectionErrorMessage = it.message.toString()) }
                    Log.e("PaymentViewModel", it.toString())
                }
        }
        
        // 신규 방식
        viewModelScope.launch(CoroutineExceptionHandler { _, e ->
            updateUiState { current -> current.copy(connectionErrorMessage = e.message.toString()) }
            Log.e("PaymentViewModel", e.toString())
        }) {
            val coupon = getCouponsUseCase()
            updateUiState { current ->
                current.copy(
                    coupons = coupon.filterAvailableCoupons(products, nowDateTime),
                    price = current.coupons.applyCoupon(products, nowDateTime),
                )
            }
        }
    }

Copy link
Author

@junseo511 junseo511 Jun 9, 2025

Choose a reason for hiding this comment

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

개인적으로 이제는 Coroutine으로 넘어오게 되면서, Result를 걷어내는 방식을 생각해보았는데, 공백의 의견은 어떠신가요~? Coroutine 에서는 아래와 같은 방법으로 Exception 에 대해서 지정을 해주는 방식도 있습니다! (힌트로 주고, 직접 찾아가도록 해볼 걸.. 이라는 생각이 들었지만 한번 바로 의견을 들어보는 것도 좋을 것 같아서, 이렇게 남겨두었습니다!)

수업 도중 코루틴에서 발생한 오류를 다루는 방법이 CoroutineExceptionHandler 라고 하였는데,
이 부분이 구체적으로 어떻게 쓰이는지는 와닿지 않았습니다 😅

두루 덕분에 이런 방식으로 코루틴 함수의 에러를 다룰 수 있다는 것을 알게 되었네요! 감사합니다 👍
반복적인 onSuccessonFailure 사용을 줄이고 뎁스도 줄이면서,
좀 더 명시적으로 '이 부분은 코루틴 함수이며, 코루틴 함수에서 에러가 발생했다' 는 것을 알려줄 수 있는 것 같아요!
심지어 Result로 감싸주지 않고 다뤄도 되어서 좋은 방법이 될 것 같기도 하구요 🤔
레포지토리 등 중간 계층에서 에러를 핸들링해야 한다면 어떻게 해야 할까에 대해서도 추가적인 고민이 되네요 🤔

위와 같은 긍정적인 이유들로 적극적인 반영을 하고 싶었으나...! 거의 3단계 미션을 새로 해야하는 리소스가 들 것 같아서
미션 마감일인 월요일까진 반영을 못할 것 같습니다 🥲
혹시 추가적으로 시간이 있다면 진행해보겠습니다 🙌

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
Author

@junseo511 junseo511 Jun 10, 2025

Choose a reason for hiding this comment

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

좋습니다~ 너무 잘해주셔서, 그렇게 코멘트 남겨둘 내용이 많지는 않았어요! 시간이 있는 만큼 천천히 한번 적용을 해보는 것도 좋을 것 같습니다

이왕 해보는 마지막 미션,,
열심히 적용해보겠습니다 🙌
cac48bb

Comment on lines 5 to 7
class OrderProductsUseCase(
private val productRepository: ProductRepository,
private val cartRepository: CartRepository,
private val orderRepository: OrderRepository,
) {
Copy link
Member

Choose a reason for hiding this comment

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

하지만, 코루틴을 적용하고 suspend fun으로 바꾸는 과정에서 로직의 길이가 크게 줄었는데,
이전에 비해 UseCase 의 효과가 감소했다는 느낌을 받았습니다.

이 부분들이 주요한 예시가 될 수 있겠군요~
개인적으로 100번 중 한 번의 유용한 사용을 위해 우리는 99번 더 만드는 것이다. 이 의견도 좋은 것 같군요!
그치만, 한가지 반문을 드려볼게요!

  1. 그로 인해서, 많은 보일러 플레이트 코드가 생긴다면?!
  2. 이러한 규칙때문에 오히려 생산성이 떨어진다면?!

여기서 우리가 얘기해볼만한 포인트가 생길 수 있겠군요~
아키텍처들과 디자인패턴등을 적용하면서, 저희는 최종적으로 어떤 것을 챙겨갈 수 있길래 하는 것일까요?!

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
Author

@junseo511 junseo511 Jun 9, 2025

Choose a reason for hiding this comment

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

이 부분들이 주요한 예시가 될 수 있겠군요~ 개인적으로 100번 중 한 번의 유용한 사용을 위해 우리는 99번 더 만드는 것이다. 이 의견도 좋은 것 같군요! 그치만, 한가지 반문을 드려볼게요!

강의 도중 나온 이야기였는데 저도 이 부분에 대해서는 동의하지 않는 편입니다!

  1. 그로 인해서, 많은 보일러 플레이트 코드가 생긴다면?!

이런 이야기를 나눌 때마다 정독하고 오는 블로그 글이 하나 있는데요,
클린 아키텍처를 무분별하게 사용하는 프로젝트가 보일 때마다 한번씩 참고하게 되더라구요 😅
그래서 저는 오히려 UseCase를 최소화하고 정말 필요한 하나씩만 만드는게 일관성을 조금 해치더라도 더 나은 방향이라고 생각합니다!

  1. 이러한 규칙때문에 오히려 생산성이 떨어진다면?!

너무 많은 규칙은 특히 초기 프로젝트에서 많은 어려움을 초래할 수 있다고 생각합니다!
규칙이 많아지면 개발자 사이의 커뮤니케이션에 많은 딜레이가 생길 것이구요 🤔
결국 요즘같은 애자일 형식의 개발 구조에서 더욱 좋지 못한 방향이 될 것이라고 생각합니다

여기서 우리가 얘기해볼만한 포인트가 생길 수 있겠군요~ 아키텍처들과 디자인패턴등을 적용하면서, 저희는 최종적으로 어떤 것을 챙겨갈 수 있길래 하는 것일까요?!

여러 개발자가 협업할 때 기능의 위치를 잘 찾을 수 있고,
작업 도중 서로의 충돌을 최소화해 유지보수에 날개를 달아주는게 가장 큰 역할이라고 봅니다 :)

저는 그래서 피처별로 모듈화를 시키고 피처별 작업을 진행하면서,
위와 같은 유즈케이스의 사용은 지양하고 추상화를 덜하는게 더 좋은 개발 방향이라고 생각되어요!

사실 더더욱 정답은 없는 부분이기에 선호하는 방향은 사람마다 다를 수 있겠지만요 🤔
두루의 의견도 궁금합니다 🙌

Copy link
Member

@Gyuil-Hwnag Gyuil-Hwnag Jun 9, 2025

Choose a reason for hiding this comment

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

크흐~ 정말 너무 좋네요!!💯💯💯

여러 개발자가 협업할 때 기능의 위치를 잘 찾을 수 있고,
작업 도중 서로의 충돌을 최소화해 유지보수에 날개를 달아주는게 가장 큰 역할이라고 봅니다 :)

맞아요~
이 부분에 대해서는 한가지 더 의견을 보태자면, 어떤 아키텍처로 구성이 되어 있다. 이 한마디만 으로도 서비스에 대한 전체적인 구조 흐름에 대해서 설명이 가능하게 되는 장점도 있었습니다

저는 그래서 피처별로 모듈화를 시키고 피처별 작업을 진행하면서,
위와 같은 유즈케이스의 사용은 지양하고 추상화를 덜하는게 더 좋은 개발 방향이라고 생각되어요!

저도 개인적으로 이렇게 생각을 합니다ㅎㅎ😀
한개의 UseCase 사용을 위해서 구조가 너무 복잡해지면, 보일러 플레이트 코드도 증가를 하게 되고 더 나아가서는 구조에 대한 개선이 복잡해지는 경우가 있었어요
추가적으로 보일러 플레이트 코드 + 복잡한 구조에 따라서 생산성도 떨어지기도 하구요

결론
개인적으로 아키텍처가 최종적으로는 생산성을 향상시켜주기 위한 것인데, 아키텍처를 지키기 위해서 생산성을 감소시킨다면 일부는 취사선택을 하는 것이 좋을 것 같다! 입니다
지나친 아키텍처 및 추상화는 오히려 확장성과 생산성을 감소하기도 시키는 경우가 종종 있게 되더라구요~

Copy link
Member

Choose a reason for hiding this comment

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

but! 저희는 현재 교육중이니까~ 분명 이에 대한 내용이 나오게 된 이유가 있을거에요ㅎㅎ
100번 중 한 번의 유용한 사용을 위해 우리는 99번 더 만드는 것이다.를 직접 경험해보고, 본인의 기준과 아이디어를 잡아가는 사람과 경험해보지도 않고, 구현을 하고 싶은대로 하는 사람과는 경험에서의 큰 차이가 있을 것 같군요

Copy link
Author

@junseo511 junseo511 Jun 10, 2025

Choose a reason for hiding this comment

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

개인적으로 아키텍처가 최종적으로는 생산성을 향상시켜주기 위한 것인데, 아키텍처를 지키기 위해서 생산성을 감소시킨다면 일부는 취사선택을 하는 것이 좋을 것 같다!

이 부분에 크게 공감합니다! 결국 개발은 예쁜 건축물을 만드는 것도 좋지만 당장 필요한 집이 없다면 오히려 독이 되니까요 :)
두루의 의견 감사합니다 🙌

본인의 기준과 아이디어를 잡아가는 사람과 경험해보지도 않고, 구현을 하고 싶은대로 하는 사람과는 경험에서의 큰 차이가 있을 것 같군요

이래서 미션을 하면서 다양한 똥과 꿀을 함께 찍어먹어보나 싶기도 하네요 😅
콜백 지옥을 모르고 suspend fun만 알았으면 코루틴의 소중함을 알지 못했을거예요,,🥺
확실히 이것저것 경험해보고 알아가는 것은 언제나 중요한 것 같습니다 :)

Copy link
Member

Choose a reason for hiding this comment

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

이 부분에 크게 공감합니다! 결국 개발은 예쁜 건축물을 만드는 것도 좋지만 당장 필요한 집이 없다면 오히려 독이 되니까요 :)

맞아요ㅎㅎ
언제나 이상과 현실은 다르더라구요~😅

저도 공백이랑 다양한 얘기들을 나누면서, 아주 즐겁게 리뷰 했던 것 같네요!
감사합니다🙇‍♂️

Comment on lines 116 to 120
viewModel.uiState.observe(this) { uiState ->
handleCatalogProducts(uiState)
handleHistoryProducts(uiState)
handlerErrorMessage(uiState)
}
Copy link
Member

Choose a reason for hiding this comment

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

오호~ UiState 형태로 적용을 해보셨군요!
사실 구현을 하신 느낌은 UiModel 느낌이기는 하군요~

장바구니 상품, 추천 상품 등 서로 동기화되어야 하는 정보가 꽤 있어서 UI 핸들링을 data class를 활용해 진행했습니다.

이 부분에 대해서 UiModel 형태로 구현이 바뀌게 된 것 같은데, 현재 보이게는 UiModel로 만듬으로서 장점이 눈에 띄는 부분이 크게 느껴지지 않았어요~
혹시 구현이 변경하게 된 흐름을 좀 더 알 수 있을까요~?

Copy link
Author

@junseo511 junseo511 Jun 9, 2025

Choose a reason for hiding this comment

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

오호~ UiState 형태로 적용을 해보셨군요! 사실 구현을 하신 느낌은 UiModel 느낌이기는 하군요~

헉 UiState보다는 확실히 UiModel이 맞겠네요! 네이밍은 변경해두도록 하겠습니다 😅
ac19953

이 부분에 대해서 UiModel 형태로 구현이 바뀌게 된 것 같은데, 현재 보이게는 UiModel로 만듬으로서 장점이 눈에 띄는 부분이 크게 느껴지지 않았어요~ 혹시 구현이 변경하게 된 흐름을 좀 더 알 수 있을까요~?

사실 저도 기존처럼 각각의 MutableLiveDate로 분리하는게 좀 더 명시적이고 각 변수를 observe하기가 용이하다는 생각을 가지고 있었으나,,,!
강의시간에 sealed class 또는 data class로 UI 상태 핸들링을 리팩토링하는 내용이 있어서 그래도 한 번 연습삼아 해보자! 라는 마인드였습니다 🤣

각 라이브데이터를 모두 분리하니 뷰모델의 부피가 커지고 그에 따른 액티비티 옵저브 코드가 늘어난다는 단점을
하나의 모델에 모아가면서 가독성 및 역할 분담에 대한 부분을 개선할 수 있었습니다.

하지만 sealed class로 해보니 장바구니의 데이터와 추천 상품의 데이터를 함께 움직이도록 하는게 조금 어렵다는 느낌이 있었고,
data class로 해보니 불필요한 관측 후 동작이 이루어질 때가 있다는 불편함이 있었습니다.

사실 각각의 장단점이 보여서 어떤 UI 핸들링 방식이 제일 나을지는 프로젝트 구성에 따라 달라질 것 같다는 결론이 드네요,,,🤔

Copy link
Member

Choose a reason for hiding this comment

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

오호~ 그렇군요!
UiState 형태로 하는 것이 좋을 때도 있기는 하지만, 어떠한 경우에는 오히려 구조를 더욱더 복잡하게 만들기도 하더라구요😅
공백의 의견 아주 좋습니다ㅎㅎ

정말 앞으로가 너무 기대되는군요~~~

Comment on lines 9 to 11
val lastHistoryProduct: HistoryProduct? = null,
val isCartProductUpdateSuccess: Boolean? = null,
val connectionErrorMessage: String? = null,
Copy link
Member

Choose a reason for hiding this comment

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

https://github.com/woowacourse/android-shopping-order/pull/110#discussion_r2120631280에서 진행한 것처럼 nullable한 데이터를 줄이고 싶은데 어떤 방법을 사용해야 할지 고민중입니다 🤔

이 부분에 대한 내용이 될 수 있겠군요~
모든 부분들에 대해서 nullable 하지 않도록 바꾸는 것은 힘들 수도 있겠군요🥲
한가지 방법으로는 Default 값을 활용하는 방법도 있을 것 같다는 생각이 들었습니다!

Copy link
Author

@junseo511 junseo511 Jun 9, 2025

Choose a reason for hiding this comment

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

이 부분에 대한 내용이 될 수 있겠군요~ 모든 부분들에 대해서 nullable 하지 않도록 바꾸는 것은 힘들 수도 있겠군요🥲 한가지 방법으로는 Default 값을 활용하는 방법도 있을 것 같다는 생각이 들었습니다!

Success 라는 변수가 false를 기본값으로 가진다면 실패부터 시작한다는 느낌이 들어서요 🤔
그래서 sealed class로 한번 결과값을 감싸볼까도 싶었지만 너무 깊은 뎁스를 가지게 되는 문제점이 있었습니다!
해당 방식의 핸들링에서 오는 어쩔 수 없는 오프셋이라는 생각도 드네요 🥲

Copy link
Member

Choose a reason for hiding this comment

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

오호 그렇군요~
사실 구현하는 사람들의 생산성을 높일 수 있는 방법이 제일 좋은 방법인 것 같다는 생각이라서, 이 부분은 현재 구현을 가져가는 것이 괜찮겠군요

@Gyuil-Hwnag Gyuil-Hwnag self-requested a review June 9, 2025 13:15
Copy link
Member

@Gyuil-Hwnag Gyuil-Hwnag left a comment

Choose a reason for hiding this comment

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

공백 안녕하세요~
해당 미션 너무 고생 많으셨습니다!
리뷰 반영 정말 잘 해주셨네요.💯

이번 미션 너무 잘해주셨습니다👍
공백과 여러 의견들을 나눠볼 수 있어서, 정말 좋았네요
마지막 미션인 만큼 내용을 살펴보시고 추가적으로 애매한 부분이 있거나 수정하고 싶으신 내용이 있다면 수정하고 요청주세요~

궁금한 점이나 논의하고 싶은 내용이 있으면 언제든 DM 주세요! 🙏
정말 고생 많으셨습니다😀

Comment on lines +132 to 136
private fun handlerErrorMessage(uiModel: CatalogUiModel) {
uiModel.connectionErrorMessage?.let {
Snackbar.make(binding.root, it, Snackbar.LENGTH_SHORT).show()
}
}
Copy link
Member

Choose a reason for hiding this comment

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

해당 부분들은 공통으로 사용할 수 있도록 Extensions 등으로 뽑아두는 방법도 있겠군요~

Copy link
Author

@junseo511 junseo511 Jun 10, 2025

Choose a reason for hiding this comment

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

해당 부분들은 공통으로 사용할 수 있도록 Extensions 등으로 뽑아두는 방법도 있겠군요~

저도 예전에는 스낵바와 토스트를 확장 함수로 빼두는 것을 선호했는데,
이렇게 단순한 한 줄짜리 로직을 분리하기 시작하다 보면 확장함수를 남발하게 되더라구요 🥲
그래서 너무 많은 확장함수가 협업하는 팀원들 사이에서 오히려 혼란을 초래한 경험이 있습니다,,

결국 XXXExtensions.kt 파일 내에 어떤 확장함수가 있는지 알아야 하고,
그렇게 되면 하나의 관리포인트가 된다는 느낌이 들었습니다!

따라서 한 줄짜리 스낵바나 토스트를 분리하기보다 직접 작성해서 확장함수의 사용을 최대한 지양하고,
앱 전역에서 디자인이나 동작의 커스텀이 필요한 특수한 경우에만
스낵바 및 토스트의 확장함수를 제작하는게 더 좋은 방법이라고 생각합니다 🙌

이에 대한 두루의 의견은 어떠신가요?

Copy link
Member

@Gyuil-Hwnag Gyuil-Hwnag Jun 10, 2025

Choose a reason for hiding this comment

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

오호~ 그렇군요!
무척이나 인정하는 부분이네요👍

사실 저도 초반에는 한줄짜리라도, 여러곳에서 사용을 안하면 Extensions로 뽑아두었는데 결국 이렇게 뽑아놓은 것들이 많아지다보니 관리 측면에서 오히려 구현이 복잡하게 보인다는 생각이 들었어요

말씀해주신대로 커스텀 스낵바 형태 or 토스트로 인한 동작 커스텀등의 경우에 사용하는 의미가 더 생길 것 같습니다!
사실 개인적으로 토스트는 Utils 로 뽑아뒀을 때는 그냥 단순하게 Toast.show() 형태가 아닌 TAG 와 Timber(Log) 수집등의 로직이 같이 있었을 때 좀 더 유용하게 활용을 했던 것 같습니다~

Comment on lines 65 to 66
Log.e("CatalogViewModel", it.message.toString())
}
Copy link
Member

Choose a reason for hiding this comment

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

CatalogViewModel과 같이 주로 사용되는 내용들은 Tag등의 상수로 만들어두고 사용을 하는 방법등도 있겠군요~

Copy link
Author

@junseo511 junseo511 Jun 10, 2025

Choose a reason for hiding this comment

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

CatalogViewModel과 같이 주로 사용되는 내용들은 Tag등의 상수로 만들어두고 사용을 하는 방법등도 있겠군요~

확실히 상수화를 해두면 좋을 것 같네요!

안그래도 PaymentViewModel에 CartViewModel이라고 써둔 태그가 있더라구요 😅
이런걸 막기 위해서 상수화는 미리미리,,, 프로젝트가 커서 추적이 어려웠다면 벌써 슬펐을 수도 있겠어요 🥲

CoroutineExceptionHandler 작업을 진행하면서 함께 적용했습니다!
cac48bb

Copy link
Member

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 +12
data class CartProductUiModel(
val cartProducts: Products = EMPTY_PRODUCTS,
val recommendedProducts: Products = EMPTY_PRODUCTS,
val editedProductIds: Set<Long> = emptySet(),
val isProductsLoading: Boolean = true,
val connectionErrorMessage: String? = null,
)
Copy link
Member

Choose a reason for hiding this comment

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

이전에도 같이 얘기를 나눴던 내용이라서, 한번 참고차 구현을 남겨보도록 할게요!
(그치만 ViewModel 에서 데이터등을 업데이트 하는 부분들이 있어서, UiState 형태로 하는 것이 좋을지는 여전히 저도 의문이긴 하네요)

data class CartProductUiModel(
    val cartProducts: Products = EMPTY_PRODUCTS,
    val recommendedProducts: Products = EMPTY_PRODUCTS,
    val editedProductIds: Set<Long> = emptySet(),
)

sealed class CartProductUiState {
    data object Loading : CartProductUiState()
    data class Success(val cartProductUiModel: CartProductUiModel) : CartProductUiState()
    data class Error(val message: String) : CartProductUiState()
}

Copy link
Author

@junseo511 junseo511 Jun 10, 2025

Choose a reason for hiding this comment

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

이전에도 같이 얘기를 나눴던 내용이라서, 한번 참고차 구현을 남겨보도록 할게요!

구현 예시 감사합니다 🙌
두루가 주신 CartProductUiState 에서 성공을 제네릭으로 감싸는 것도 괜찮은 것 같아요!

이전에 코멘트를 나누었던 것처럼 여러 데이터가 연관되고,
계속 업데이트를 해줘야 한다면 UiState 형태로 진행하는 것이 독이 될 수도 있겠어요 🤔

장바구니 액티비티 등에서는 개별적인 LiveData로 두는 것이 유지보수에 제일 용이해 보이네요 🤔
아니면 장바구니 Fragment와 추천 상품 Fragment의 뷰모델을 분리해서 또 다른 구현 방식을 채택하는 것도 좋아보이구요 :)

Copy link
Member

@Gyuil-Hwnag Gyuil-Hwnag Jun 10, 2025

Choose a reason for hiding this comment

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

크흐~ 활용할 수 있는 예시까지 생각해내시다니..! 이사람 정말 탐나네요

이전에 코멘트를 나누었던 것처럼 여러 데이터가 연관되고, 계속 업데이트를 해줘야 한다면 UiState 형태로 진행하는 것이 독이 될 수도 있겠어요 🤔

맞아요! 업데이트가 자주 일어나야 하는 경우에는 캐스팅등에 대한 판별등이 들어가게 되거나 해서 오히려 기존처럼 나누어서 작업했으면 빠르게 작업할 수 있던 내용들이 복잡한 상태에 엮이게 되는 경우들이 있었어요~

장바구니 액티비티 등에서는 개별적인 LiveData로 두는 것이 유지보수에 제일 용이해 보이네요 🤔
아니면 장바구니 Fragment와 추천 상품 Fragment의 뷰모델을 분리해서 또 다른 구현 방식을 채택하는 것도 좋아보이구요 :)

오호~ 사실 제가 구현을 하는 것이었다면 그렇게 했었을 것 같다는 생각이 드는군요
각각의 Fragment 에 ViewModel 을 두고, 공통으로 사용하는 데이터에 대해서는 Activity 의 activityViewModel 을 활용하는 방법을 채택했을 수도 있겠군요

Comment on lines +76 to +77
Toast.makeText(this, getString(R.string.payment_order_success), Toast.LENGTH_SHORT).show()
finish()
Copy link
Member

Choose a reason for hiding this comment

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

SnackBar 와 마찬가지로 해당 부분들도 Extensions로 뽑아둔다면, 추후에 유용하게 사용을 해볼 수도 있겠군요~

Copy link
Author

Choose a reason for hiding this comment

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

SnackBar 와 마찬가지로 해당 부분들도 Extensions로 뽑아둔다면, 추후에 유용하게 사용을 해볼 수도 있겠군요~

이 코멘트에 대한 답변은 토스트 관련 코멘트에 남겨드리겠습니다 🫡

@junseo511
Copy link
Author

두루 덕분에 좀 더 꼼꼼하게 개념을 활용하고,
크게 의식하지 않는 부분까지 잘 챙겨갈 수 있었던 것 같습니다 🙌

현업에서 활동하는 두루의 의견을 직접 들어볼 수 있어
더욱 즐거웠던 마지막 미션이 된 것 같네요 😄

혹시 인사를 드릴 기회가 없을까,
리뷰 요청을 드리면서 미리 인사 남겨드립니다‼️
두루의 소중한 시간을 내서 리뷰해주셔서 감사합니다 :)

@Gyuil-Hwnag Gyuil-Hwnag self-requested a review June 10, 2025 13:38
Copy link
Member

@Gyuil-Hwnag Gyuil-Hwnag left a comment

Choose a reason for hiding this comment

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

공백 안녕하세요~
미션 너무 고생 많으셨습니다! 👏
리뷰 반영 정말 잘 해주셨어요💯

레벨2 마지막 미션 정말 고생 많으셨습니다~
이번 미션을 진행하면서, 공백과 같이 이야기를 주고 받으면서 생각을 알 수 있어서 정말 좋았습니다!
구현을 하시면서 여러가지 고민을 하셨을 텐데 다음번 레벨에서도 잘 적용을 하면서 발전해 나가시길 바라겠습니다😆

이번 미션은 여기서 마무리하고 머지하도록 하겠습니다.
도움이 될만한 내용들 코멘트 달았으니 확인해보셔도 좋을 것 같습니다!

궁금한 점이나 논의하고 싶은 내용이 있으면 언제든 DM 주세요! 🙏
앞으로도 응원하겠습니다💪
레벨2 단계 동안 정말 고생 많으셨습니다👏

Comment on lines +11 to 12
suspend fun getProducts(
@Query("category") category: String? = null,
Copy link
Member

Choose a reason for hiding this comment

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

물론 잘 아시겠지만, Coroutine 을 적용하면서 주된 내용중 하나가 fun vs suspend fun 일 수 있겠군요~
이 부분에 대해서는 추후 suspend function 이 어떻게 만들어지고, 어떠한 원리로 동작하는지도 살펴본다면 좀 더 코루틴을 유용하게 사용해볼 수 있겠다는 생각이 듭니다💪

Comment on lines +75 to +76
Log.e("[CouponResponse]", "유효하지 않은 쿠폰 타입이 존재합니다!")
null
Copy link
Member

Choose a reason for hiding this comment

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

한번 참고차 남겨두었습니다~
Log 에는 종류들이 여러가지가 있습니다!
Log.i, Log.w, Log.e 등등 이는 각 로그에 대해서 남기는 내용에 대한 중요도를 나타내기도 합니다
이에 대해서 공백의 기준을 잘 세우신다면, 실제 개발을 할 때 좀 더 유용하게 로그등을 남기고 이러한 로그를 바탕으로 서비스 안정성을 잘 가져갈 수 있도록 도움을 줄 수 있겠다는 생각이 듭니다💪

Copy link
Member

Choose a reason for hiding this comment

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

추가로 Debug시에만 로그가 보여지도록 하는 Timber 라는 라이브러리도 존재를 합니다~
실제로 서비스를 할 때 많이 사용을 하는 라이브러리이기도 해서, 이런 것이 있다! 느낌으로 한번 살펴보는 것도 좋을 것 같다는 생각이 듭니다

Comment on lines +98 to 105
viewModelScope.launch(
CoroutineExceptionHandler { _, e ->
Log.e(TAG, e.message.toString())
},
) {
val product = uiModel.value?.catalogProducts?.getProductByProductId(productId) ?: return@launch
increaseCartProductQuantityUseCase(product)
loadCartProduct(productId)
Copy link
Member

Choose a reason for hiding this comment

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

CoroutineExceptionHandler 의 활용 아주 좋네요💯
이 부분에 대해서는 한가지 내용을 참고차 드려볼까 합니다~
이러한 경우에는 Base 를 사용해보는 방법도 있겠군요!

abstract class BaseViewModel : ViewModel() {
    abstract val tag: String

    val defaultCoroutineExceptionHandler = CoroutineExceptionHandler { _, e ->
        Log.e(tag, e.message.toString())
    }
}

class CartViewModel( ... ): BaseViewModel() {
    override val tag: String = "CartViewModel"

    fun increaseCartProductQuantity(productId: Long) {
        viewModelScope.launch(defaultCoroutineExceptionHandler) {
            val product = uiModel.value?.cartProducts?.getProductByProductId(productId) ?: return@launch
            val newQuantity = increaseCartProductQuantityUseCase(product)

            updateUiModel { current ->
                current.copy(
                    cartProducts = current.cartProducts.updateQuantity(product, newQuantity),
                    editedProductIds = current.editedProductIds.plus(productId),
                    isProductsLoading = false,
                )
            }
        }
    }
}

Comment on lines +101 to 102
private const val TAG: String = "ProductDetailViewModel"
val Factory: ViewModelProvider.Factory =
Copy link
Member

Choose a reason for hiding this comment

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

TAG의 상수화 정말 좋습니다~👍

Comment on lines 49 to -50
android:onClick="@{() -> viewModel.toggleAllCartProductsSelection()}"
app:isGone="@{viewModel.cartProducts.products.empty}"
Copy link
Member

Choose a reason for hiding this comment

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

참고차 한가지 팁을 남겨볼까 합니다~
버튼등 클릭에는 연속으로 클릭(연타)를 하는 경우도 있으니, 아래의 개념들을 활용하여 SingleClick 이 되도록 하는 방법들이 있을 것 같군요!
debounce / throttle

@Gyuil-Hwnag Gyuil-Hwnag merged commit e243f2f into woowacourse:junseo511 Jun 10, 2025
1 check passed
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.

2 participants