티스토리 뷰

728x90

 

 

미션 관련 링크

영화 티켓 예메 레포지토리

https://github.com/kimhm0728/android-movie-ticket
영화 티켓 예매 1, 2단계 PR

https://github.com/woowacourse/android-movie-ticket/pull/72

영화 티켓 예매 3, 4단계 PR
https://github.com/woowacourse/android-movie-ticket/pull/83

 

 

 

코드 리뷰 받은 코멘트들 

액티비티 테스트에서 다른 액티비티로의 이동, 앱 종료 등은 테스트하지 않는다

도메인 테스트에서도, 해당하는 객체에 대해서만 테스트했고 다른 객체는 테스트하지 않았듯이
액티비티 테스트에서는 한 액티비티에서의 시나리오만을 검증한다.

Ui 테스트에서는 하나의 시나리오에 관점을 두고 진행한다.
ex: 정상적으로 데이터가 노출된다. (데이터 일치 검증 포함), 데이터를 받아오지 못했다면 에러 화면에 노출된다.

 

 

화면 별로 패키지를 생성하자

contract, activity를 기준으로 패키지를 분리했을 때, 화면이 100개라면 activity 패키지 내에 100개의 activity 파일이 존재해야 한다.

이 방법은 빠르게 원하는 액티비티를 찾기 힘들다.

 

 

ListView, RecyclerView의 어댑터는 MVP에서 View의 역할을 할까?

Adapter는 MVP의 View가 아닌, Android 프레임워크 내의 View를 관리해주는 객체다.

즉, Adapter는 MVP 계층 내에 포함되어 있지 않으므로 Presenter와 상호작용하지 않는다. 

 

 

하나의 액티비티에서 Activity의 Intent 설정을 바꿔가며 Ui Test를 하는 방법

액티비티를 테스트하기 위한 방법으로 ActivityScenarioRule만을 알고 있었다.

테스트 클래스의 상태값으로 ActivityScenarioRule를 생성하고, intent를 전달하기 때문에
하나의 클래스 내에서 intent를 변경해가며 테스트를 어떻게 해야할지 몰랐다.

ActivityScenario.launch { ..Activity.newIntent(..) }

이때 위와 같은 방법으로 Intent 설정을 바꿔줄 수 있다.
예를 들어 Intent로 정상적인 데이터를 받아왔을 때의 시나리오, 데이터를 받아오지 못해 에러가 발생했을 때의 시나리오 각각을 테스트하고 싶을 때 활용할 수 있겠다.

 

 

도메인 모델을 ui 에서 그대로 사용하는 것이 적절할까?

하나의 dto를 도메인 모델에서도 사용하고, 액티비티나 어댑터에서도 사용했었다.

이렇게 설계하는 경우 dto(도메인 모델)과 뷰가 강하게 결합된다.

만약 ui 형식이 변경된다면, dto를 변경해야 한다.

도메인 모델은 도메인에서만 사용하고, 이 도메인 모델을 뷰에서 사용할 수 있게 가공하는 작업을 따로 구현해야 한다.

여기서 UiModel을 추가하거나, Domain -> View로 가공해주는 함수(Mapper)를 추가한다.

 

UiModel 관련 랑크 https://reddragonnest.github.io/posts/ui-model.html

 

 

에러는 공통으로 관리하지 않는다

화면마다 발생하는 에러가 다르고, 종류가 매우 다양하기 때문이다.

화면마다 ErrorType을 관리하고, 그 화면에 해당하는 액티비티에서 ErrorType별로 대응한다.

안드로이드 첫 미션을 하면서.. 에러를 어떻게 처리할지에 대한 고민을 정말 많이했다.

 

처음에는 공통적인 에러 리스너 하나를 생성해서, 해당 에러가 발생할 수 있는 화면에서 리스너를 구현하도록 했다.

하지만 이 방법은 추후 기능이 추가되고 발생할 수 있는 에러가 많아질 경우 유지보수에 좋지 않다.

결과적으로는 화면마다 발생할 수 있는 에러들을 정의한 sealed interface를 만들고,
그 화면에서 에러별로 어떻게 처리할지를 각각 구현했다.

 

 

startActivity() vs newIntent()
class MovieReservationActivity :
    BaseActivity<MovieReservationContract.Presenter>(),
    MovieReservationContract.View {
    
    //...
 
    companion object {
        private val TAG = MovieReservationActivity::class.simpleName
        // ...

        fun startActivity(
            context: Context,
            movieId: Long,
        ) {
            Intent(context, MovieReservationActivity::class.java).run {
                putExtra(MOVIE_ID_KEY, movieId)
                context.startActivity(this)
            }
        }
    }
}

액티비티를 시작하기 위해 전달받아야 할 데이터들을 누락하는 것을 방지하기 위해,

Intent를 생성하는 코드를 해당하는 액티비티에서 구현했다.

처음에는 startActivity()라는 함수를 생성해서, Intent를 생성하고 바로 액티비티를 시작했다.

 

        fun newIntent(
            context: Context,
            movieId: Long,
        ): Intent {
            return Intent(context, MovieReservationActivity::class.java)
                .putExtra(MOVIE_ID_KEY, movieId)
        }

그런데 액티비티를 바로 시작하지 않고 데이터를 Intent에 담기만 하고, 그 Intent를 반환하는 방식도 있다는 것을 알게 되었다.

Intent를 반환하는 방식은, 액티비티를 언제 실행할지에 대해서 그 액티비티를 호출하는 곳에서 결정할 수 있다.

 

 

mockk를 활용한 Presenter 테스트에서, view의 구체적인 값 검증까지 하는 방법

presenter 테스트에서, view의 어떤 함수가 호출되는지만 테스트하는 것은 너무 광범위한 테스트라고 생각했다. 

대부분의 테스트에서 verify 내부에서 뷰의 인자로 any()를 전달해주었기 때문이다.

presenter 테스트를 어느 정도까지 해야할지 모르겠어서 리뷰어님께 질문을 드렸다.

 

view 함수가 호출될 때의 인자 값을 검증할 수 있도록 "캡처링"을 활용해보라고 하셨다.

캡처링을 통해 presenter 테스트에서도 도메인 테스트와 유사하게, presenter의 로직을 거친 후 예상되는 값(expected)과 실제 값(actual)이 일치하는지 검증할 수 있다.

 

presenter 테스트에 캡처링을 적용해보면서, view 인자로 값을 직접 지정해주는 것과 어떤 차이가 있는지 잘 몰랐다.
또한 간단한 값을 테스트할 때는 (ex: "예약 인원이 3명일 때 인원을 증가시키면 4명이 된다"와 같은 테스트는 뷰의 인자로 4만을 넣어주면 됨) 캡처링하는 방식이 오히려 테스트의 복잡도를 높힌다고 생각했다.

 

이러한 생각들을 말씀드렸더니, 리뷰어님이 나의 의문들을 바로 해결해주셨다! 아래는 리뷰어님의 답변이다.

 

1. slot이 아닌, mutableList을 사용하면 캡처한 history를 모두 저장한다.
view의 인자로 들어간 여러 값들과 그 순서까지 검증할 수 있다.

 

2. assert와 verify는 역할이 다르다.

assert는 정확한 값을 검증하기 때문이다. verify로 값을 넣어주는 경우, 어떤 값이 호출되는지를 정확하게 알아야 한다. 

(ex: 1000개의 데이터를 가진 리스트가 인자로 전달되는 경우, 그 리스트를 view의 인자로 넣어 verify해줘야 하므로 verify를 사용할 수 없음)

 

3. 모든 presenter 테스트를 캡처링을 사용하지 않아도 된다. 간단한 로직인 경우 verify로도 충분히 검증 가능하다.

 

캡처링 참고 링크 https://mockk.io/#capturing

 

 

Repository에서 객체의 id를 직접 부여해주지 않는다

이전 미션에서 배운 Dao 느낌으로 Movie 객체들을 관리해주는 Movies라는 객체를 구현했었다.

이후 Movies라는 네이밍이 Movie의 일급 컬렉션과 같은 느낌이 들어서, MovieRepository로 수정했다.

 

사실 안드로이드 아키텍처의 Repository를 잘 알지 못했다. 근데 왜 이런 네이밍을 했나?!?!
그냥... Movie 객체들을 저장하고, 삭제하고, 불러오는 등의 작업을 해주고,
다른 몇몇 크루들이 Repository 패턴을 사용했기 때문에 "아 이게 Repository구나!"라는 안일한 생각이었다.

 

그런데 리뷰어께 Repository에서 id를 직접 부여해주는 것이 어색하다는 코멘트를 받았다.

리뷰어님과 DM으로 길게 얘기를 주고받다가.... Repository 정의부터 정확히 짚고 넘어가야 할 것 같다고 말씀하셨다.

 

일단 Repository는 다른 데이터 계층을 추상화해주는 역할을 한다.

Ui 계층에서는 데이터 계층을 직접 접근하지 않고, Repository를 통해서만 데이터를 받아오는 것이다.
Ui 계층은 데이터를 서버에서 가져오는 건지, 내부 DB에서 가져오는 건지 알지 못한다.

만약 현재 미션에서 영화 데이터들을 서버에서 가져온다고 했을 때, 당연히 서버에서 Movie dto에 대한 id가 부여된 상태일 것이다.

그런데 Repository에서 id를 직접 부여하고 있으니 확실히 이상했다.

현재는 서버가 없으므로, Movie 객체를 임시로 생성해준 dummy data에서 id를 부여하도록 수정했다.

Repository에서는 id가 이미 부여되어있는 Movie들을 단순히 저장하기만 한다!!!


사실 강의에서 다루지 않은 내용이라.. 현재 단계에서 안드로이드 아키텍처에 깊게 고민할 필요는 없는 것 같다.

그런데 잘 알지도 못한 상태에서 MovieRepository라는 네이밍을 부여했으니 ㅋㅋ 당연히 코드를 읽는 입장에서는 혼란스러울만 하다..

(바보 같은 나에게 친절히 가이드 주신 리뷰어님께.... 압도적 감사... TT)


Repository 참고 링크 https://developer.android.com/topic/architecture/data-layer?hl=ko#architecture

 

 

TextView의 textSize는 sp? dp?

우테코 이전에는 보통 dp를 사용했었다.
크기가 고정되어있는 버튼 등에서 sp를 사용하면, 폰트 크기 설정에 따라 글자가 잘릴 수 있다는 이유에서였다.

그런데 일반적으로 텍스트 사이즈는 sp로 두며,
TextView를 감싸고 있는 뷰의 사이즈는 wrap_content로 두고 margin이나 padding을 통해 크기를 조정한다는 것을 알았다.

뷰 사이즈를 dp 또는 일정 비율로 고정했다고 해서 TextView의 사이즈까지 dp로 고정할 필요가 없었다!

그냥 뷰 사이즈를 wrap_content로 지정해주면 되는 것이다.....!!!!! ㄷㄷ

 

Presenter 함수명에는 뷰의 로직을 구체적으로 명시하지 않는다

MVP의 핵심은 "Presenter는 View를 구체적으로 알지 못하는 것"이다.

이 핵심에 따라, Presenter의 함수명도 View에 의존적이지 않게 네이밍해야 한다.

만약 Presenter의 함수명이 clickReservationButton()이라면,

View에서 버튼이 아닌 다이얼로그로 변경되었을 경우 Presenter의 함수명까지 변경해야 한다.

이 경우 Presenter가 View를 구체적으로 알고 있는 것이기 때문에 적절하지 않다.

+ Presenter 테스트에서도, 테스트명은 뷰에 의존적일 수 없다.

 

 

 

리뷰어/페어 피드백

리뷰어 피드백

- 안드로이드가 거의 처음이라고 했지만 기본적인 코틀린에 대한 지식이 있으셔서 그런지 잘 따라와주셨습니다.

- 현재 미션의 본질적인 목표를 1순위로 두어 미션에 임하는 것이 필요함. 다른 아키텍처나 기술도 좋지만, 지금 경험할 수 있는 것들을 익힌 후에 습득한다면 지금보다 더 이해하기도 좋을 것.

- 적극적으로 커뮤니케이션을 시도해주셔서 좋았음. 많은 리뷰 핑퐁도 중요하겠지만, 충분히 스스로 고민을 해보고 어떠한 시도를 해보고 실패를 했는지를 알아가는 과정도 중요.

 

페어 피드백

- 기본적으로 코틀린을 잘 다루고 있기에 안드로이드만 지금처럼 열심히 공부하면 모든 면에서 완벽해질 것 같음.

- 적극적으로 어떤 부분은 좋고 어떤 부분은 어떠한 근거로 좋지 않은 것 같다고 잘 이끌어 주어서 편하게 미션을 진행할 수 있었음.

 

 

영화 티켓 예매 미션을 끝내고

시간이 갈수록 TMT(투머치토커..)가 되는 기분이다.

회고글도 그렇고, PR 코멘트도 그렇고 계속 할말이 많아진다. ㅋㅋㅋ

코드에 대한 욕심이 나는 것도 있고~ 조금이라도 의문이 드는 점이 있다면 바로 리뷰어님께 질문하는 습관이 생기기도 했다.

그리고 그 질문을 기록하면 더 오래오래 남으니까~ 이렇게 뚱뚱한 회고글이 되었다.....

 

이번 미션을 진행하며 느낀점을 4가지 주제로 정리해보았다.

 

 

코멘트를 여러번 주고받고 싶은 나의 욕심 🫠

오목 미션 때 리뷰 요청 마감 전날에 부랴부랴 PR을 날린 적이 있다. 리뷰어님과 코멘트를 여러번 주고받지 못해 굉장히 아쉬웠다.

그래서 레벨2의 목표는, 완벽하지 않아도 일단 리뷰 요청을 드리고! 내가 구현한 내용에 대해서만 코멘트를 받는 것이다.

그리고 부족한 부분은 리뷰 요청을 기다리면서 구현하면 된다고 생각했다.

 

이번 미션에서는 요구 사항(ui 테스트, 화면 회전)을 다 구현하지 않고 리뷰 요청을 드렸다.

물론 리뷰 요청을 드리기 전에 리뷰어님께 말씀드리긴 했지만.....

요구 사항도 제대로 구현하지 않은 코드를 읽어야 하니, 리뷰어님 입장에서는 리뷰하기 어려웠을 수도 있다.

지금 생각해보니 내 욕심 때문에 부담을 드린 것 같아 죄송하다......

 

미션에서는 요구 사항 구현이 가장 최우선임을 다시금 깨닫는다.

개인적인 욕심으로 리팩터링을 진행하고 구조적으로 아주 예쁜 코드가 되어도, 요구사항을 만족하지 않으면 무용지물이다..

지금은 우테코 미션이지만, 회사에서 이런 행동을 한다면 내 신뢰는 점점 떨어지지 않을까;;^^

 

 

일단 질문하자!

    override fun onCreate(savedInstanceState: Bundle?) {
        // ..
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
        // ..
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when (item.itemId) {
            android.R.id.home -> finish()
        }
        return super.onOptionsItemSelected(item)
    }

툴바에 있는 뒤로가기 버튼을 위와 같은 방식으로 구현했다.
그리고 이 뒤로가기 버튼의 아이디는 android.R.id.home이다.


나는 해당 시나리오를 테스트 하고 싶었다.

즉, “툴바의 뒤로가기 버튼을 누르면 ~~ 액티비티로 이동한다”라는 테스트를 작성하고 싶었다. 

        onView(withId(android.R.id.home))
            .perform(click())

        onView(withId(R.id.이전으로 이동할 액티비티의 뷰 아이디))
            .check(matches(isDisplayed()))

 

그런데 android.R.id.home은 액티비티의 xml 내에 선언된 id가 아니기 때문에, view를 찾을 수 없다는 에러가 발생했다.
엄청난 삽질을 해봐도 해결되지 않았고.. 결국 레아께 질문드렸다.
그런데 '이건 안드로이드 프레임워크를 테스트하는 것일 수 있다'라는 답변을 얻었다. (그리고 위에서 작성했듯, 액티비티 테스트에서는 다른 액티비티로 이동하는 시나리오를 테스트하지 않는다.)

테스트 가능한 시나리오와 불가능한/불필요한 시나리오를 구분하는 것이 중요하며, 현재 단계에서 이를 구분하는 것이 어려운 게 당연한 것이라고 하셨다..

 

사실 레아께 질문을 할지 고민을 했다. 코치님들은 우리가 스스로 정답을 찾아가라는 의도로, 명확한 답을 알려주시지 않기 때문이다.

그런데 덕분에 새로운 시각으로 바라볼 수 있게 되었고 큰 도움이 되었다!

앞으로 사소하고 바보같은 질문도 일단 던져봐야겠다는 생각을 하게 된 삽질이었다.. ^^

 

 

바쁘다 바빠 현대사회

확실히 레벨1 보다 더~~~~ 바빠진 것을 체감했다.

계속 코드에 대한 욕심이 나서, 새벽 4시까지 코딩하고, 일어나자마자 또 코딩하고 그랬다. ㅋㅋㅋ

 

안드로이드에게 억까 당하고.. 계속 삽질해서 구현에 더 오래걸린 것도 있다.

억까라고 하기엔 내 사소한 실수들이긴 하지만.. 0L을 0으로 쓰거나, 액티비티명 안 바꾸거나, Junit5로 import 잘못하거나 등등;;

안드로이드를 제대로 배운 적이 없고 새로운 개념들이 많아서 그런지 삽질을 정말 많이 했다.

 

그래서 더 우선순위에 집중하는 것이 중요한 것일 수 있겠다.

안드로이드 프레임워크는 범위가 정말 넓기 때문에, 심연에 빠지면 끝도 없다.

시간 내에 미션을 구현해야 하는 상황에서 API 하나하나 뜯어볼 여유는 없는 것 같다.

최소한의 API 동작 원리만 배우고, 이를 잘 활용하는 것도 아주 중요한 역량임을 느낀다!

 

 

잘하려는 욕심보다 배우려는 욕심!

무슨 차이인가 싶겠지만... 나에게는 정말 큰 차이다.

"잘하려는 욕심"은 레벨1의 나였고, "배우려는 욕심"은 레벨2의 나다.

 

레벨1 때는 코틀린 위주로 학습했는데, 나는 다른 크루들보다 코틀린을 깊게 공부했다고 생각했다.

그래서 남들보다 잘 하는 것이 당연하고, 잘해야 한다는 욕심이 있었다.

이런 내 욕심 때문에 레벨1 때는 심적으로 조금 힘들었던 것 같다.

리뷰어한테 칭찬받고 싶고 얼른 머지됐으면 좋겠고.... 남들보다 뒤처지는 것 같으면 속상했다.

 

그런데 레벨2의 나는 상대적으로 건강한 욕심을 가지고 있다.

다른 크루들보다 안드로이드 경험이 현저히 적기 때문에, "못하는 것이 당연하지!"라는 마인드다.

매일 새로운 개념들을 배워가고 이를 적용해나가는 것이 굉장히 재밌다!!!!!!

 

안드로이드를 잘 모르기 때문에 오는 이점도 있는 것 같다.

미션과 벗어나는 길로 빠지지 않고, 미션에서 요구하는 개념들에만 집중할 수 있기 때문이다!

이번 페어도 나랑 비슷하게 안드로이드 경험이 많이 없었는데,

페어와 안드로이드 개념을 코드에 차근차근 적용하고 점차적으로 코드를 개선해나가는 과정이 정말 재밌었다.

안드로이드를 잘 아는 페어였다면, 페어가 알고 있는 지식을 따라가느라 힘들었을 것 같다..

 

 

 

 

아 진짜 회고 끝끝끝 

말이 정말 많지만 줄일 자신이 없다

어쨌든 레벨2 너무 즐거워용

 

728x90
250x250
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
글 보관함