ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • NextStep Android Architecture with TDD 1기 - 1 MVC, Domain, Multi Module Project
    안드로이드 2021. 8. 10. 00:53
    반응형

    이 글은 NextStep의 안드로이드 아키텍처 with TDD 교육 과정을 듣고 쓴 후기 시리즈이다.

    Effective Kotlin 1기 과정을 수강하고 1년 뒤, 이 과정을 수강할 수 있는 좋은 기회가 있어 참여하게 되었다.

    이 과정을 수강하면서 배운 내용을 정리하면서 후기를 남기면, 안드로이드 테스트에 대해 관심 있는 다른 사람들에게도 꽤 쓸만한 글이 될거라 생각되어서 글을 쓰기 시작했다.

     

    1편 링크 - NextStep Android Architecture with TDD 1기 - 1 MVC, Domain, Multi Module Project [지금 보고있는 곳]

    2편 링크 - NextStep Android Architecture with TDD 1기 - 2 MVP Architecture with Test

    3편 링크 - NextStep Android Architecture with TDD 1기 - 3 MVVM Architecture with Test

     

    아래는 해당 과정의 링크이다. next step의 강의는 가격대가 좀 있는 편이지만, 이 가격이 결코 아깝지 않을 정도로 많은 깨달음을 얻고 간다.

    이 글을 보는 모든 사람들이 이 강좌를 들으면 좋겠다! (돈 안 받았습니다 😅)

    https://edu.nextstep.camp/c/QT9zj8KN


    강의를 들으며

    멀티모듈 프로젝트라는 것을 작년 말에 친구를 통해서 알게 된 다음부터 관심을 가지기 시작했었는데, 제대로 활용을 하지는 못하고 있었구나 하고 생각하게 되었다.

    안드로이드 아키텍처의 레이어를 모두 멀티모듈로 나눌 수 있다는 사실!!!!! 이 인사이트를 강의를 듣고 얻게 되었다.

    인사이트를 얻은 것만으로도 많은 생각을 할 수 있어서 좋았다.

    • 그럼 의존성 주입은 어디서 해야하지?
    • 순환 의존성이 발생할 수도 있지 않을까?
    • 기타 등등...

     

    좀 더 생각해서 결론이 난 것들도 있었지만, 지금 진행하고 있는 프로젝트부터는 모두 레이어 별로 모듈을 나누면서 겪어보자! 하고 결론을 냈다.

     

    사이드 프로젝트 하고 있는 친구들 모두 멀티모듈로 리팩터링 해봐야겠다 ㅎㅎ

     

     

    도메인 로직

    아직도 도메인 로직의 경계가 어디인가를 잘 모르겠다.

    아무래도 그런 경계를 생각하지 않고 로직을 짜왔기 때문이 아닐까 하고 생각해봤다. 너무 아키텍처 레이어에 연연해서 도메인 로직에 대해서는 신경쓰지 않았다.

     

    Effective Kotlin 과정을 들을 때도, 도메인 로직은 순수 언어로만 짜는 것이 좋다고 했었는데 말이다.

     

    아무튼, 강의를 들으면서 이 도메인 로직 또한 멀티모듈로 나눠서 분리 관리 해야한다는 또 다른 인사이트를 얻게되었다.

    인사이트를 하나씩 얻을 때마다 내가 코드를 바라보는 시각이 엄청나게 넓어지는 느낌이 많이 들었다. 아, 왜 이런 것들을 생각하지 못했을까!

     


    미션 시작!

    첫 주차 미션은 Effective Kotlin 과정에서 했던 문자열 계산기를 만들고, 해당 클래스를 활용해서 UI 를 구성하고, UI 테스트 코드를 작성하는 것이었다.

    UI 테스트라니!

     

    맨날 공부 해야지, 해야지. 했던 녀석인데 이번 기회에 접할 수 있어서 아주 좋았다.

     

     

    1단계 미션

    첫번째 미션은 Effective Kotlin 문자열 계산기 구현과 똑같았다. 그래서 별 다른 어려움 없이 쓱쓱 해결했다.

    다른 점이 있었다면

    1. 인텔리제이에서 작성한 게 아니라 안드로이드 스튜디오에서 작성했다는 점
    2. 순수 kotlin 모듈로 분리해서 작성했다는 점
    3. Junit4를 사용했다는 점.

    1, 2번은 참 신선한 경험이었다. 당장 다른 사람들이 쓸 라이브러리를 만들어내는 기분이 들었다. 나도 드디어 도메인 로직을 따로 짜보는 경험을 하는구나!

     

    3번은 안드로이드는 Junit4를 공식 지원한다는 점이다. Junit5를 사용을 할 수는 있으나, 제약이 많고 Rule에 해당하는 클래스들을 직접 만들어주어야한다. (매우 귀찮다).

     

    게다가 테스트를 굴리려면 설정해야할 게 매우 많다. 그래서 그런지 안드로이드에서 Junit5를 사용할 수 있게 해주는 3rd party 라이브러리가 존재한다.

    이전 안드로이드 테스트 코드를 작성했던 모든 프로젝트에서 저 써드파티 라이브러리를 사용했었다.


     

    미션을 수행 하면서, 기능들이 단순해서 테스트 종류가 얼마 없었지만... Parameterized Test 하는 게 매우 불편했다.

    가독성도 썩 좋지도 않았다. 

    // Junit 5
    @CsvSource("1,2,ADD,3", "3,1,MINUS,2", "2,4,MULTIPLY,8", "12,3,DIVIDE,4")
    @ParameterizedTest
    fun `계산 전략에 따른 계산 결과 확인`(value1: Double, value2: Double, operator: Operator, result: Double) {
        // given
        val operand1 = Operand(value1)
        val operand2 = Operand(value2)
    
        // when
        val expectedOperand = operator.calculate(operand1, operand2)
    
        // then
        assertThat(expectedOperand).isEqualTo(Operand(result))
    }

    Junit 5 에서는 위 처럼 깔끔하게 작성할 수 있던 코드였지만....

    // Junit 4
    @RunWith(Parameterized::class)
    class OperatorCalculateParameterTest(
        private val operator: Operator,
        private val left: Operand,
        private val right: Operand,
        private val expectedResult: Operand,
    ) {
        companion object {
            @JvmStatic
            @Parameters(name = "{0} 연산자의 계산 전략 수행 테스트 : {1} {0} {2} = {3}")
            fun tokenAndOperators(): Collection<Array<Any>> {
                return listOf(
                    arrayOf(Operator.PLUS, Operand(1), Operand(2), Operand(3)),
                    arrayOf(Operator.MINUS, Operand(3), Operand(2), Operand(1)),
                    arrayOf(Operator.MULTIPLY, Operand(2), Operand(5), Operand(10)),
                    arrayOf(Operator.DIVIDE, Operand(10), Operand(2), Operand(5)),
                )
            }
        }
    
        @Test
        fun `각 연산자에 맞는 계산 전략에 맞게 계산을 수행한다`() {
            // when
            val actualResult = operator.calculate(left, right)
    
            // then
            assertThat(actualResult).isEqualTo(expectedResult)
        }
    }

    우웩. 🤮🤮🤮

    이게 정말 최선이었다.... Junit4 에서는 가독성도 안좋고 작성하기가 너무 불편했다.

    어차피 domain 로직에는 안드로이드 의존성이 전혀 없으니 (없어야 하니), 도메인 모듈에서는 Junit 5를 계속 쓰려고 한다 😅

     

    1단계 Pull Request Link

     

     

    2단계 미션

    2단계 미션은 아주 단순한 UI 테스트 입문이었다. 숫자를 누르면 해당 숫자가 텍스트 뷰에서 보이게 짜는 것 뿐이었다.

    @Test
    fun 버튼_1을_누르면_텍스트뷰에_1이_보여야한다() {
        // when
        onView(withId(R.id.button1)).perform(click())
    
        // then
        onView(withId(R.id.textView)).check(matches(isDisplayed()))
    }

    이런식으로 매우 단순한 테스트였다.

     

    2단계 Pull Request Link

     

     

    3단계 미션

    문자열 계산기에 대한 모든 요구사항을 UI 테스트로 작성하는 미션이었다. 예를들면,

    1. 수식이 456 일때 지우기 버튼을 누르면 45이 된다
    2. 수식이 '4 +' 일때 * 를 누르면 '4 *' 로 변한다.
    3. 수식이 비어있을 때, 연산자를 누르면 아무 변화가 없어야한다
    4. 기타 등등 더 많다.

    UI 테스트를 하기 이전에 연산자와 피연산자로 이루어진 수식 이라는 도메인 로직을 만들어야했다. 그리고 매우 객체지향적인 설계를 하고싶었다.

    두뇌 풀가동을 해서 열심히 설계를 해봤다.

     

    그렇게 해서 나온 초안.

    지금은 조금 다르지만 이런 넉김으로 설계를 시작했다. 그리고 이 객체를 TDD를 통해서 만들었다.

    처음에는 중복 코드도 매우 많고 너저분했는데, 리팩터링을 계속 진행하면서 지금은 생각보다도 더 만족하는 로직을 짤 수 있었다.

     

    객체 이름을 Calculator Memory 라고 지었는데, Expression이 제일 나았다. 왜냐면 계산 기록을 다음 미션에서 만들어야 하는데 네이밍이 겹치기 때문이다. ㅋㅋㅋㅋㅋㅋ Expression이 훨씬 더 직관적이기도 하다!

     

    도메인 로직을 모두 짰으니, 다음으로는 UI 테스트들을 모두 만들고 실패하게끔 만들어뒀다.

    그리고 이 도메인 로직을 액티비티 코드에 끼워넣으면서 테스트를 모두 성공하게 만들었다.

    도메인 로직만 잘 짜두면 View에서는 보여주기만 하면 되는 문제라서 매우 간단했다. 물론 UI 테스트 작성하는 게 처음이라 너무 어색해서 오래 걸리긴 했지만 말이다.

     

    UI 테스트를 할 때 Toast 메시지를 테스트하는 거에서 조금 헤맸는데, 검색을 통해 좋은 글을 발견해서 그 로직을 사용했다.

    class ToastMatcher : TypeSafeMatcher<Root>() {
        override fun describeTo(description: Description?) {
            description?.appendText("is toast")
        }
    
        override fun matchesSafely(root: Root): Boolean {
            val type: Int = root.windowLayoutParams.get().type
            if (type == WindowManager.LayoutParams.TYPE_TOAST) {
                val windowToken: IBinder = root.decorView.windowToken
                val appToken: IBinder = root.decorView.applicationWindowToken
                if (windowToken === appToken) {
                    //means this window isn't contained by any other windows.
                    return true
                }
            }
            return false
        }
    
        companion object {
            @JvmStatic
            fun isToast(): ToastMatcher = ToastMatcher()
        }
    }

    위 처럼 커스텀 Matcher인 ToastMatcher 클래스를 만들어서 아래처럼 테스트를 진행했다.

    ...
    // when
    onView(withId(R.id.buttonEquals)).perform(click())
    // then
    onView(withText("완성되지 않은 수식입니다")).inRoot(isToast())
            .check(matches(isDisplayed()))

    3단계 Pull Request Link


    마무리

    Effective Kotlin 1기를 수강할 때는 백수여서 1주일에 미션 1개씩 뿌시는 게 그리 어렵지 않았다.

    그런데 지금은 회사를 다니다보니, 2주에 한 번 강의를 하시는데도 생각보다 타이트했다. (하고있는 다른 일들이 너무 많아서 그런 것도 있다 🤣)

    처음 미션은 UI 테스트 때문에 시간이 걸렸고, 다음 부터는 더 어려울텐데 시간을 더 투자해야겠다는 생각이 많이 들었다.

     

    또, 생소한 UI 테스트를 해보고, 자동으로 실제 UI를 클릭하면서 돌아가는 내 폰을 보면서 매우 신기했다.

    테스트 하다가 내가 직접 뭐 누르면 테스트 실패하겠지...? 😅

     

    내 주변 안드로이드 개발자들도 객체지향에 눈 떴으면 좋겠다는 생각을 많이 했다. 내 주변에는 이상하게도 테스트와 객체지향에 관심을 가지는 사람들이 없다.

    점점 나와 같은 생각들을 하는 사람들과 교류하며 일하고 함께 성장하는 상상을 자주 하게된다.

     


     

    이 글이 도움이 되셨나요?

    말리빈에게 커피를 한 잔 쥐어 주세요! ☕ 커피를 인풋으로 더 좋은 글을 아웃풋으로 쓰려 노력하겠습니다 😊

     

    Buy Me A Coffee

     

    반응형

    댓글

Designed by Tistory.