티스토리 뷰
미션 관련 링크
쇼핑 장바구니 레포지토리
https://github.com/kimhm0728/android-shopping-cart
쇼핑 장바구니 1, 2단계 PR
https://github.com/woowacourse/android-shopping-cart/pull/54
쇼핑 장바구니 3, 4단계 PR
https://github.com/woowacourse/android-shopping-cart/pull/79
코드 리뷰 받은 코멘트들
일반적으로 뷰모델은 화면과 1:1 구조로 만든다
흔히 화면 단위의 ViewModel은 액티비티와 1:1 대응 구조를 가지고는 합니다.
뷰모델의 역할은 프레젠터와 마찬가지로 데이터(UI 상태)를 관리하고, 비즈니스 로직을 수행하는 것이다.
그렇기에 현재 "UI에서 비즈니스 로직을 수행하고 있는 것은 아닌지?", "뷰모델의 테스로 검증을 해야 하는 것은 아닌지?"와 같은 고민이 필요하다.
화면과 1:1 구조를 가지는 뷰모델로 명확히 만든 다음, 각 케이스에 대한 로직의 뷰모델 테스트를 작성한다.
의존성 추가 시 implementation testImplementation androidTestImplementation를 분리한다.
예를 들어 espresso는 안드로이드 테스트에만 사용되므로 androidTestImplementation로 추가한다.
뷰모델에서의 테스트 더블
프로덕션 코드에는 Repository의 save()라는 기능이 필요하지 않다.
테스트 환경에서 원하는 데이터를 넣어주기 위해 생성했는데, 이는 테스트를 위해 생긴 기능이기 때문에 좋지 않은 설계다.
테스트를 위한 Repository를 생성할 때, Repository의 생성자로 저장할 데이터를 넣어준다.
class FakeProductRepository(private val savedProducts: List<Product>) : ProductRepository {
// ...
}
화면 회전 시 데이터를 다시 로드하는 문제점
뷰모델에서 데이터를 로드하지 않고, Activity와 같은 뷰의 onCreate()에서 viewModel.loadXXX()를 호출했다.
이 경우 화면 회전 시 뷰모델은 아직 살아있는데 뷰는 다시 생성되기 때문에 동일한 데이터가 다시 로드되는 문제가 발생할 수 있다.
loadXXX()를 viewModel의 init 블럭 내부에서 호출할 수도 있고, Event, SingleLiveData 등을 통해 해결할 수 있다.
뷰모델이 실패하는 상황을 뷰가 핸들링하는 것이 적절할까?
만약 어떠한 기능이 비동기로 동작하여 오류가 발생한다면, 오류가 발생했다고 화면에 보여줘야 한다.
이러한 오류 핸들링 또한 비즈니스 로직이기 때문에 뷰모델이 해야 할 역할이다.
실패를 일반화하지 말자
장바구니 상품이 없는 경우, 다른 처리를 해야 했다.
Repository에서 상품을 find하고, 찾으려는 id의 상품이 없는 경우를 예외 상황으로 판단했다.
즉 find 함수를 runCatching으로 감싸고, 예외가 발생한 경우에만, 특정 처리를 해주었다.
하지만 상품이 없는 경우는 오류가 아니다.
명확하게 아이템이 없는 경우와 정말로 오류가 난 것을 구분할 수 있어야 한다.
프로덕션 코드와 테스트 코드에서의 코드를 명확하게 구분
class RoomProductRepository(
private val productDao: ProductDao,
savedProducts: List<Product> = emptyList(),
) : ProductRepository {
기존 코드는 위와 같았다.
이 미션에서는 서버에서 데이터를 가져오지 않았고, 로컬에 저장된 dummy 데이터를 넣어주는 식이었다.
생성자를 통해 임의로 생성해준 dummy 데이터를 넣어주기 위해 savedProducts라는 프로퍼티를 추가했다.
하지만 RoomProductRepository는 프로덕션 코드이기에 savedProducts가 있는 것이 어색하게 된다.
인터페이스로 작성한 이유 자체가, 테스트 환경에서 테스트 더블을 활용하기 위함인데,
프로덕션 코드에 savedProducts가 있는 이유를 다른 사람이 납득하기 어렵다.
Room 사용 시 데이터베이스 객체를 하나만 만든다
SQL 같은 데이터베이스를 구축할 때도, 데이터베이스 인스턴스 자체를 여러개 만들지 않고 테이블을 여러개 만들었다.
테이블 만큼의 데이터베이스를 만들지 않고, 하나의 데이터베이스 객체만을 만든다.
페이지 이동 시, 현재 페이지를 나타내는 변수를 변경할 시점
fun moveNextPage() {
_page.value = _page.value?.plus(1)
loadCart()
}
fun movePreviousPage() {
_page.value = _page.value?.minus(1)
loadCart()
}
기존에는 위 코드와 같이 _page라는 LiveData를 먼저 변경하고 loadCart()를 통해 새로운 페이지 데이터들을 불러왔다.
하지만 새 페이지를 불러오는 과정에서 오류가 발생하면, 페이지는 이동하지 않았는데도 페이지 값은 변경되어 있는 경우가 발생한다.
즉, 페이지를 새로 불러온 이후에 페이지 값을 변경해야 한다.
UI 상태와 UI 이벤트를 분리
수량을 더하는 레이아웃이 계속 중복되었고, 이 레이아웃을 재사용하다보니 해당 레이아웃의 바인딩 객체에 들어가는 <수량 감소 리스너/수량 증가 리스너/수량>을 하나의 클래스로 묶었다.
하지만 이 타입에 맞추기 위한 불편한 로직들이 만들어졌고, 수량 감소 리스너/수량 증가 리스너 등이 상태가 되었기 때문에 이 기능에 대한 테스트도 못하게 되었다.
또한 이 클래스가 슈퍼유틸이 되었다.
즉, UI 상태와 UI 이벤트를 분리해야 한다.
여러개 화면에서 재사용 되는 것은, 데이터 모델이 아닌 UI입니다.
가장 raw 레벨의 바인딩 객체가 <수량 감소/수량 증가/수량> 세 개의 인터페이스로 동작하게 변경한다.
(상황에 따라 기존과 같은 구조가 좋을 수 있지만 기본적인 연습부터 경험하기 위함)
아이템 리스너 네이밍
리스너 인터페이스의 함수 네이밍을 deleteCartItem, increaseQuantity, decreaseQuantity와 같이 해주었다.
그런데 이 리스너는 실제 UI에서만 사용되는 기능이기 때문에 오히려 UI 관련된 사항들만 네이밍에 포함시키는 것이 좋다.
실제 동작은 리스너의 구현체에 맡기는 경우가 많다.
예를 들어 아이템을 클릭했을 때, 어떤 화면에서는 상세로 가고 어떤 화면에서는 다른 화면으로 이동하는 등 동작이 달라질 수 있기 때문이다.
다만 뷰모델이 이러한 리스너 인터페이스를 직접 구현하면, 함수 네이밍이 UI와 너무 의존적이기 때문에 불편한 점이 있다.
이런 경우 뷰모델에서 직접 구현하지 않고, 뷰를 한번 더 타도록 작성해서 해결할 수 있다.
ex)
class MainActivity : Listener {
override fun onClick() { viewModel.~~() }
}
deleteCartItem, increaseQuantity, decreaseQuantity -> onItemClick, onIncreaseClick, onDecreaseClick
리뷰어/페어 피드백
리뷰어 피드백
- 구현을 비롯해 전반적으로 놓치는 것 없이 꼼꼼하게 작성하려는 모습이 좋았음.
- 댓글마다 커밋에 대한 기록을 남겨주어 좋았음.
페어 피드백
- 기능 구현을 잘 함. 코틀린의 장점을 잘 활용해서 많이 배웠음.
- 본인의 의견을 적극적으로 제시하고, 상대방의 의견도 적극적으로 물어봐주었음. 우선 순위를 두고 일을 처리하는 습관이 좋았음.
쇼핑 장바구니 미션을 끝내고
다음 미션이었던 쇼핑 주문과 더불어서, 심적으로 제일 힘들었던 미션이었다.
우테코 와서 내 불안정한 마음이 조금은 성숙해졌다고 느꼈는데 아니었나보다. 이때만큼 너덜너덜했던 적이 없다..
일단 안드로이드 경험이 없는 만큼, MVVM을 처음 다뤄봤다.
뷰와 1:1 대응되지 않도록 뷰모델을 생성했는데, 이 구조를 뜯어고치느라 미션 초반부터 정말 많은 시간을 소모했다.
이미 MVVM에 익숙한 크루들이 대부분이었기 때문에, 다른 크루들은 나보다 더 유의미한 리뷰를 주고받고 있다는 생각에 마음이 너무 조급했다.
레벨2에서 더더욱 느낀 거지만 난 항상 조급해 진다.
남들보다 잘 하고 싶고 남들보다 앞서나가고 싶다. 최대한 많은 것을 배우고 싶은 욕심도 있다.
그래서 조금 더 무리해서라도 리뷰를 빨리 제출하고, 리뷰어와 티키타카도 많이 하려고 노력했다.
그런데 이번엔 거의 가장 늦게 미션 merge가 됐다.
페어가 아닌 혼자 진행했던 3, 4단계 미션에서도 구현 자체에 어려움을 느꼈다.
이때까지 구현 자체에 어려움을 느낀 적은 없었기 때문에 혼란스러웠다.
이 과정이 너무 힘들었기 때문에 '난 개발에 재능이 없어', '난 왜 개발에 흥미를 느끼지 못할까?' 라는 생각까지 했다.
다른 크루들한테 접근 방법을 물어보고, 코드도 참고해서 어찌저찌 제출하긴 했지만, 지금봐도 많이 부족한 코드다..
우테코 내에서, 내가 남들보다 뒤처지고 있다는 걸 이렇게 진하게 느낀적은 처음이다..
근데 못하는 게 당연하지 않나?
나는 상대적으로 경험이 부족한데도 남들보다 잘하기를 바라고 있었다. 정말 욕심덩어리였다..
못하는 것도 당연하고, 이 과정에서 힘듦을 겪는 것도 당연하다.
하나도 힘들지 않다면 성장하고 있지 않는 것이다.
그런데 힘들어하는 나자신한테 또 스트레스를 받았다. 그냥 내가 약한 사람인게 싫었다. (인간은 원래 약한데...)
미션을 모두 마무리한 지금, 내 감정과 내 피드백들을 다시 떠올려보면 모두 당연하게 고민해야했던 것들이다.
내 PR에 달린 코멘트들을 다시 쭉 읽어봤는데, 너무 당연하게까지 느껴진다.
그만큼 코멘트들 모두 내것이 되었다는, 성장했다는 증거가 아닐까!
그때의 힘듦이 있었기에 성장할 수 있었다!
앞으로는 힘들어도 즐기자.. 힘들어도 버티자! 누구나 힘들 수 있잖아!!!!
'app > woowacourse' 카테고리의 다른 글
[우아한테크코스] 레벨3 학습 돌아보기 인터뷰 회고 (5) | 2024.08.29 |
---|---|
[안드로이드/우아한테크코스] MVP의 Presenter에서는 context를 알아도 될까? (0) | 2024.06.28 |
[우아한테크코스] 안드로이드 레벨2 영화 극장 선택 미션 회고 (3) | 2024.05.17 |
[우아한테크코스] 안드로이드 레벨2 영화 티켓 예매 미션 회고 (1) | 2024.05.07 |
[우아한테크코스] 안드로이드 레벨1 오목 미션 회고 (0) | 2024.04.12 |