마이리얼트립 안드로이드 앱의 Technical Debt 해결하기
100% Kotlin, Next Architecture Pattern, Testability 개선을 위한 청사진
위 문장을 들으면 어떤 기분이 드시나요? 가슴이 막 먹먹해지고 분노가 치밀어 오르고 어쩌면 눈물이 한줄기 또르르 흐를 수도 있겠죠. 짧은 문장이지만 가지고 있는 무게감은 절대 가볍지 않습니다. 하지만 말입니다. 그 많은 일이 내가 좋아하는 일이라면 어떨까요?
Prologue
저는 마이리얼트립에서 안드로이드 앱을 개발하고 있는 최재호라고 합니다. 이제 입사한지 한 달을 갓넘은 뉴비이기도 하죠. 그 한달간 마이리얼트립의 안드로이드 앱을 다각도로 분석하며 내린 결론은 이 글의 첫 문장 그대로 할일이 많다는 것입니다. 물론 전 좋습니다. 행복해요. 꿀벌이 아름다운 꽃을 보고 자연스럽게 이끌리듯 전 일을 찾아 떠나온 꿀벌이니까요.
하지만 사실 전 이렇게 할 일이 많을 것이라고 입사 전까지 상상하진 못했습니다. 시간을 거슬러 마이리얼트립에 채용 전형이 한창 진행 중일 때 저는 입사 이후 이 앱에 어떤 기여를 할 수 있을까 힌트를 얻고자 마이리얼트립 안드로이드 앱을 ‘디컴파일하고 프로파일링하고 레이아웃 인스펙터’를 사용해가며 분석했습니다. 이 앱이 사용한 라이브러리를 확인하고 대략 어떤 구조를 사용했는지, 레이아웃은 효율적으로 배치되었는지 메모리 누수는 없는지를 확인했습니다. 그리고 높은 완성도에 감탄하고 현 마이리얼트립 안드로이드 개발자에게 찬사를 보내며 입사하고 내가 과연 할일이 있을까 그 걱정을 했었습니다. 돌이켜 생각해보면 세상에서 제일 쓸데없는 걱정이었지만요.
Goals
마이리얼트립 모바일 팀에 명문화된 목표는 아직 없지만 제가 세운 개인적인 목표는 이렇습니다.
“무자비한 추가/변경 요청에 여유 있게 대응 할 수 있는 유연하면서도 견고한 구조의 앱 개발”
(삶도 사람도 코드도 여유는 꼭 필요한 덕목이라고 생각합니다.)
변화가 잦은 e-commerce 그것도 일정에 민감한 여행 관련 상품을 다루는 플랫폼의 특성상 불규칙적으로(대부분의 개발자가 매우 좋아하는 단어중 하나죠) 들어오는 요청이 많고 시의적절하게 기능이 추가되어야 하는 경우도 많습니다. 이런 변화를 대처하기 위해서는 유연한 대응이 가능한 구조가 필수적이겠죠. 그리고 그런 유연한 변화에도 흔들림 없이 신뢰할 수 있는 견고함은 기본이 되어야 할 것입니다.
그럼 지금 마이리얼트립의 앱은 이런 목표에 잘 부합하고 있을까요? 이미 언급했다시피 마이리얼트립 안드로이드 앱은 훌륭한 완성도를 가지고 있습니다. 이건 내부자가 아닌 외부자일때 내렸던 비교적 객관적인 평가입니다. 하지만 놀랍게도 더 훌륭해 질 수 있는 가능성이 여전히 남아있었습니다. 더 유연하며 더 견고해 지기 위해 앞으로 마이리얼트립 앱은 어떤 일을 벌여볼 예정인지 지금부터 하나씩 살펴보겠습니다.
“기술 부채 (Technical Debt)”라는 용어가 있습니다. “부채(負債)”가 이자를 내고 돈을 빌려 쓰는 것처럼 기술 부채는 해결되어야 할 기술적 이슈를 뒤로 미루고 비즈니스 문제를 해결하는 시점을 당기는 것을 의미합니다. 아래 소개될 대부분의 항목은 가용 리소스 부족으로 해결하지 못한 기술 부채에 가깝습니다.
Java + Kotlin — Java = Kotlin
마이리얼트립 안드로이드 앱은 Kotlin이 활성화 되기 이전 개발되었습니다. 새로 개발되고 있는 새 기능은 Kotlin을 사용하며 개발되고 있지만, Java와 Kotlin이 혼용되고 있는 대부분의 프로젝트가 그렇듯 Kotlin의 강점을 모두 다 살리지 못하고 있는 상황입니다. 함께 사용하고 있는 Java 어르신을 배려하기 위해 불필요하게 작성되어야 하는 코드나 사용할 수 없는 기능들이나 Java와 Kotlin을 번갈아 가며 코딩할 때 놓치는 ;와 또는 불필요하게 들어 가는 ; 등 사소하지만 차곡차곡 쌓이면 그 자체로도 스트레스가 되기도 합니다.
그 밖에도 요즘 주목받고 있는 Coroutine 이나 Dagger의 대체재로 떠오른 Koin 을 사용하기에도 부담스럽습니다.
만나고 싶다. Coroutine
현재 마이리얼트립 안드로이드 앱은 대부분의 요즘 프로젝트들이 그렇듯 RxJava2 기반으로 비동기 처리를 하고 있습니다. 요즘 RxJava와 Coroutine을 비교한 여러 아티클이 “Forget RxJava” 라던가 “you should really consider replacing RxJava with Kotlin(Coroutine)” 이런 결론으로 귀결되고 있어 실제 프로젝트에 적용했을때의 효과에 대해 기대하고 있습니다.
이런 저런 이유를 굳이 찾지 않아도 Kotlin이 안드로이드 개발에 있어서 Java에 비해 생산성이 높은 언어라는 점에 대해서는 이견이 없다고 생각합니다. 마이리얼트립 안드로이드 앱은 100% Kotlin 앱으로의 전환을 목표로 하고 있습니다.
MVP + MVVM -> ?
글을 쓰기 전 안드로이드 디자인 패턴(android design pattern)을 대한 글을 구글에 검색해 보니 0.44초에 565,000,000건의 검색 결과가 나왔습니다. 안드로이드 개발자들이 디자인패턴에 얼마나 관심이 많은지 보여주는 단적인 지표라고 보입니다. 안드로이드에 디자인 패턴 적용이 태동하던 무렵 마이리얼트립는 MVP 디자인 패턴을 적용하여 개발되었습니다. MVP는 아주 훌륭한 디자인 패턴이지만 Google 트렌드로 확인할 수 있는 바와 같이 최근 몇 년 동안 데이터 바인딩을 접목한 MVVM 패턴이 주목 받았고 항상 더 나은 구조를 위해 고민하던 우리팀은 구글 앱 아키텍처에서 권장하는 AAC와 MVVM을 도입하기로 했습니다.
하지만 안타깝게도 기존에 구현되어있던 MVP 구조도 아직 건재합니다. 많은 프로젝트가 그러하듯(우리만 그런 건 아니죠? 제발…) MVP에서 MVVM으로 넘어가는 큰 흐름 속에 두 개의 거대한 디자인 패턴이 한 프로젝트에 공존하고 있는 셈입니다. 다양성을 중요한 덕목으로 생각하는 저로서도 그 둘의 불편한 공존은 견디기 어려웠고 우리 소중한 프로젝트가 더 아름다운 모습으로 거듭나기 위해 일관성 있는 하나의 구조에 대해 끊임없이 고민하고 있습니다. 그런 고민에는 MVP, MVVM, MVI, Clean Architecture 또는 추후 선보일 Jetpack의 Compose를 기반으로 한 Declarative UI Pattern 등 모든 가능성을 열어두고 검토하고 있습니다.
하지만 그전까진 현재 사용 중인 MVVM을 제대로 사용해야겠죠? Kotlin과 마찬가지로 신규 개발되는 항목들은 MVVM을 적용하여 개발하고 있습니다. 또한 기존에 작성된 MVVM 패턴 코드에서 아쉬운 부분들에 대해서 개선 작업을 진행하고 있습니다.
그 항목은 다음과 같습니다.
1. ViewModel이 Android 종속성을 가진 코드를 참조하고 있는 점 — Testablility 를 저해하는 요소입니다.
2. AndroidViewModel을 ViewModelProvider를 통하여 생성하지 않는 경우 — ViewModel에 데이터 전달을 위해 직접 인스턴스를 생성하여 사용하고 있는 경우가 있습니다.
3. (어쩐지 Repository 패턴이 적용되어 있지 않습니다) ViewModel에서 바로 데이터에 접근하고 있는 점 — Testablility를 떨어뜨리며 네트워크나 DB등 데이터를 접근하기위한 수단을 개선하는데 어려움이 있습니다.
Testability 개선
전 테스트 코드를 “제대로” 작성하는 일이 대부분의 코드상 이슈를 해결하는 데 도움이 된다고 믿습니다. 중요하니 한 번 더 말합니다. “제대로” 입니다. 테스트 코드를 작성하는 건 한없이 쉬울 수 있지만 제대로 된 테스트 코드를 작성하는 일은 그리 녹록하지 않습니다. 특히나 안드로이드 프로젝트의 테스트는 까다롭습니다. 이유는 여러 가지가 있겠죠. 안드로이드 테스트라는 꼭지를 가지고도 한편의 글이 나올 만큼 이야기할 내용은 많지만 간략하게 알아보자면 이런 이유가 있습니다.
1. 테스트를 위해 제공되는 툴이 난해합니다.
ActivityTestCase, ActivityUnitTestCase, ActivityInstrumentationTestCase2 등 이름으로는 뭐하는지 각각 무엇이 다른지 알 수 없는 정체 불명의 도구들이 많습니다. 내 Application 객체의 Function 하나를 테스트 하기위해, 또는 내 Activity의 동작을 테스트하기 위해 무엇을 선택해야 하는지부터 고민스러워집니다.
ActivityUnitTestCase — 레이아웃 및 격리 된 메서드를 테스트하는 데 사용 ActivityInstrumentationTestCase2 — 터치/마우스 이벤트를 보내고 Activity의 상태 관리를 테스트하려는 경우 사용
ActivityTestCase — 일반적으로 는 사용하지 않습니다.
Android API-level 24부터 위 세개의 클래스는 Deprecated 되었습니다. 현재는 Android-Testing-Support-Library사용을 권장합니다.
2. 심지어 테스트가 느립니다!
“apk 패키징 > apk 설치 > 실행” 과정이 선행 되어야 하므로 저의 PC처럼 시스템 자체가 빠르지 않은 경우가 아니더라고 해도 Android의 테스트 프레임워크는 기본적인 java 테스트보다 이미 느립니다.
3. UI 테스트는 원래 어렵습니다.
이건 비단 Android만의 이슈는 아닙니다. iOS도 웹도 PC 애플리케이션도 UI 생성과 이벤트를 다루는 코드는 테스트하기 어려운 분야입니다. 또한 그 효용성에 대해서도 갑논을박이 많은 분야입니다. UI 객체의 속성은 자주 바뀌고 대부분 비동기로 동작하며 익명 클래스 등을 통해 처리되는 이벤트는 추적하기가 어렵습니다.
4. Mock 객체를 사용하기 굉장히 까다로운 구조입니다.
Android는 기본적으로 android.test.mock 패키지를 통해 MockContext, MockApplication, MockResource와 같은 기본적인 Mock 객체를 제공합니다. 하지만 이들의 구현체는 UnsupportedOperationException만 뱉는 사용하기 어려운 껍데기입니다. 만약 필요하다면 이 Mock 객체를 직접 구현해야 하는데 이건 쉬운 일이 아닙니다.
위와 같은 테스트 제약사항을 개선해보고자 우리 팀은 다음과 같은 테스트 정책을 실행하려고 합니다.
- JVM 로컬 단위 테스트 진행
로컬 단위 테스트는 JVM에서 실행되며 안드로이드 테스트에 비하여 빠르게 동작합니다. 안드로이드 개발자 페이지의 문서에는 다음과 같이 소개하고 있습니다.
컴퓨터의 로컬 JVM(Java Virtual Machine)에서 실행되는 테스트입니다.
테스트에 Android 프레임워크 종속성이 없거나 Android 프레임워크 종속성에 대한 모의 객체를 생성할 수 있는 경우 이 테스트를 사용하면 실행 시간을 최소화할 수 있습니다.
런타임에 이 테스트는 모든 final 한정자가 삭제된 수정된 버전의 android.jar에 대해 실행됩니다.
소개글을 보니 로컬 단위 테스트는 앞서 단점으로 지적된 느린 테스트 속도를 충분히 보완해 줄 수 있어 보입니다. 추가로 안드로이드에서 제공하는 InstrumentationTest 툴을 사용하지 않아 스트레스도 훨씬 적습니다. 과거 JUnit 으로 테스트를 작성했던 분들은 입문도 한결 쉽게 가능합니다. 좋습니다. 하지만 뒤에 한마디가 더 붙어 있습니다. “런타임에 이 테스트는 모든 final 한정자가 삭제된 수정된 버전의 android.jar 에 대해 실행됩니다.” 이 의미는 무엇일까요?
Android Studio를 통하여 로컬 단위 테스트 태스크를 실행해 보면 태스크 도중에 createMockableJar (구 mockableAndroidJar) 태스크를 호출하는것을 확인 할 수 있습니다.
이 태스크가 위 소개글에서 설명하는 android.jar 파일을 mock 인터페이스 형태로 컴파일하는 태스크 입니다. 문제는 android.jar가 사실상 아무런 기능을 하지 않는다는 것입니다. 아마도 처음 테스트 코드를 작성하며 Android 프레임워크에 기능을 사용하게 되면 이런 에러 메시지를 만난 경험이 있을 것입니다.
java.lang.RuntimeException: Stub!?
이런 부분을 해결하기 위해서는 Stub으로 처리된 부분을 실제로 구현해야 합니다. 하지만 이건 테스트를 위한 테스트 코드 작성을 요구하여 테스트의 무결성에 영향을 줄 뿐 아니라 무엇보다 굉장히 번거롭습니다.
꼭 코드 작성하지 않아도 해당하는 라이브러리를 직접 추가해 줄 수도 있습니다. 예를 들어 Android 프레임 워크에 포함된 org.json.JSONObject 객체를 사용하며 Stub!? 이 발생한 경우 JSONObject 라이브러리를 추가하여 해결 할 수도 있습니다.
이런점을 개선하기 위해 시도되고 있는 여러 프로젝트 중 가장 많이 사용하는 것이 Robolectric 입니다. Robolectric은 JVM에서 Android SDK가 제공하는 코드를 가로채 정상 실행될 수 있도록 동작합니다.
Android 프레임워크에 대한 종속성을 Robolectric이 어느 정도 해결해 준다고 하더라도 말 그대로 로컬(JVM)에서 동작하는 만큼 Android Runtime 환경과 차이가 발생 할 수 있습니다. 로컬 테스트는 근본적으로 다른 환경이라는 걸 인지하고 본질적인 코드 검증에 사용해야 합니다.
2. Mockito를 활용한 행위 검증(expected-run-verify)
실용주의 프로그래머 저자 데이비드 토머스와 앤드류 헌트는 실용주의 프로그래머를 위한 단위 테스트 with JUnit란 저서에서 이상적인 단위 테스트 작성을 위하여 필요한 Independent에 대해 이렇게 정의하고 있습니다.
테스트는 깔끔함과 단정함을 유지해야 한다. 즉, 확실히 한 대상에 집중한 상태여야 하며, 환경과 다른 개발자들에게서 독립적인 상태를 유지해야 한다.
또한 독립적이라는 것은 어떤 테스트도 다른 테스트에 의존하지 않는다는 것을 의미한다. 어느 순서로든, 어떤 개별 테스트라도 실행해 볼 수 있어야 한다. 처음 것을 실행할 때 그 밖의 다른 테스트에 의존해야 하는 상황을 원하지는 않을 것이다. 모든 테스트는 섬이어야 한다.
출처 : 실용주의 프로그래머를 위한 단위 테스트 with JUnit
이렇게 단위와 단위 사이 격리를 위해 테스트 대상이 되는 단위에서 사용하는 다른 단위를 대체하기 위해 사용하는 것을 테스트 더블이라고 합니다. 그 중 행위 검증에 주로 사용되는 mock을 활용하기 위해 우린 Mockito Framework을 사용할 계획입니다.
Mockito는 자바에서 단위테스트를 하기 위해 Mock을 만들어주는 프레임워크입니다.
Mock이 필요한 테스트에 직관적으로 사용할 수 있도록 만들어졌습니다.
출처: https://github.com/mockito/mockito/wiki/Mockito-features-in-Korean
지금까지 마이리얼트립 안드로이드 앱이 앞으로 개선해 나아갈 사항들을 몇 가지 간추려 알아봤습니다. 이 밖에도 정리된 코딩 컨벤션 적용이나 내부적으로 준비 중인 오픈소스 프로젝트를 비롯하여 Dark-Theme과 같이 새로운 안드로이드 버전이 나오면서 적용해야 하는 기능들도 줄지어 순서를 기다리고 있습니다. 그것으로 끝이 아닙니다. 아직은 팀 규모가 작아서 시작하지 못하고 있는 팀 내 스터디와 세미나도 있습니다.
쌓여있는 일을 생각하면 밤에 자려고 누웠다가도 웃음이 피식피식 새어 나옵니다. 왜냐면 전 이 일을 정말 좋아하기 때문입니다. 좋아하는 일을 해도 해도 끝이 없으니 이보다 더 좋을 수 있을까요?
좋은 것을 나누는 것은 우리 조상님들로부터 대대로 전해 내려온 미덕입니다. 그래서 이 좋은 것을 여러분들과 나누고 싶습니다. 마이리얼트립은 여러 복지와 쾌적한 근무환경을 자랑하지만 가장 내세울 만한 건 바로 저처럼 이 일을 아끼고 사랑하는 동료들과 함께 할 수 있다는 점이 아닐까 싶습니다.
밴드 오브 브라더스의 Easy 중대에 지원한 한 병사는 공수부대에 지원한 이유가 무엇이냐는 질문에 “최고와 함께 싸우고 싶어서”라고 대답했습니다. (우연일까요? 저도 공수부대 출신입니다.) 마이리얼트립에는 여러분과 함께할 최고의 동료들이 있습니다. 그리고 무엇보다 좋아할 만한 일이 충분히 준비되어 있습니다. 어서 오세요. 여기 지금 할 일이 많습니다.