육아휴직 기념(?)으로 2025년 요즘 기준의 안드로이드 앱 개발 환경을 간단히 소개해본다.
빌드 : gradle
bazel 얘기가 좀 나오고 있긴 하지면, 여전히 굳건한 대세는 gradle 이다. 대단히 크거나 복잡한 프로젝트가 아니라면 괜히 고생하지 말고 gradle 을 쓰자.
빌드 설정: convention plugin
요즘은 하나의 모듈이 아닌, 하나의 앱 모듈과 여러 라이브러리 모듈로 구성하는게 일반적이다. 이러다보면 여러 모듈의 build.gradle 에 동일한 설정이 중복될 수 밖에 없다. 예전엔 그냥 apply from: common.gradle
이런식으로 다른 gradle 파일을 직접 참고하기도 했지만, 요즘은 좀 더 우아하게 convention plugin 을 만들어서 공유한다.
의존 관리 : version catalog
여러 모듈의 의존 라이브러리 버전을 잘 관리하기 위해 예전엔 build config 를 쓴다거나 이런 저런 꼼수를 썼으나, 이젠 version catalog 가 표준이다.
여러 모듈에서 동일한 의존들을 줄줄이 적어야한다면, convention plugin 이나, version catalog 의 bundle 기능을 사용하면 깔끔하개 정리할 수 있다. 예를 들어 compose 를 사용하는 모듈이라면 딸려오는 의존들이 매우 많은데, version catalog 에 compose bunlde 을 만들어서 딱 한줄로 해결하거나, compose convention plugin 을 만들어 의존은 plugin 다 넣어버릴 수도 있다.
build cache 도입
gradle 은 빌드된 결과물을 캐싱한다. 이를 build cache 라고 한다. 모듈 에 변경이 없을 경우엔 다음 번 빌드 시 해당 모듈을 다시 빌드하지 않고 캐시를 이용해 산출물을 가져오기 때문에 훨씬 빠르다.
build cache 는 local cache 와 remote cache 로 나뉜다. 혼자 개발한다면야 local cache 만 있어도 충분하겠지만, 여럿이 개발을 한다면 remote cache 를 도입한다면 다른 팀원의 빌드 시간을 줄일 수 있다. remote cache 를 구성하려면 캐시 서버가 있어야 하는데, build- cache-node 라는 도커 이미지를 제공하기 때문에 손쉽게 구성할 수 있다. 다만 당연히 도커 이미지의 스토리지는 어딘가에 영속적으로 남겨져 있어야 캐시로써 의미가 있을것이다.
remote build cache 의 경우, 보수적인(?) 조직이라면 CI 서버에서만 cache server에 push 하도록 정책을 잡을 수도 있다.
빌드 성능 분석 : build-scan
gradle의 build scan 기능을 사용하면 빌드 시간을 더 줄일 수 있는 힌트를 얻을 수 있다. 안드로이드 스튜디오에도 빌드 분석 기능이 있긴 하지만, build scan 은 좀 더 세밀한 분석결과를 제공한다.
문제는 build scan 의 결과가 로컬이 아닌 gradle 서버에서 제공한다는 것이다. 정확하게 gradle 서버에 뭘 전송하는지는 문서를 더 봐야겠지만, 보안문제가 생길 수 있다. 리포트 지우기 등의 기능도 제공하지만, 회사에서 쓰려면 보안 유지를 위해 문서를 잘 읽어보자. gradle 의 유료 솔루션인 develocity 를 쓰면 이런 문제를 해결할 수 있지만 비용이...
모듈 구성 : feature 별 분리
모듈 구성은 layer 별로 한다 / feature 별로 한다 / 둘 다 한다 등의 논쟁이 많은데 일단 난 feature 파이다. 반박 시 이건 내 글이니 내 말이 맞다. 뭐 그 안에서도 공유되는 모델들도 있고 해서 복잡하겠지만. 여튼 난 feature 를 기준으로 모델을 분리한다.
대표적인 모듈 분리의 이점은 다음과 같다.
- kotlin 의 internal 접근자를 이용해 내부 구현을 숨길 수 있다.
- build cache 등을 이용해 빌드 속도를 줄일 수 있다.
네트워킹 - retrofit
요즘은 KMP(kotlin multiplatform) 때문에 ktor 얘기도 나오는 것 같은데, 난 retrofit 이 익숙해서 여전히 retrofit 에 한표를 던진다. 물론 KMP를 염두한다면 ktor 로 가보는 것도 좋겠다. 아무거나 상관없다고 생각한다.
retrofit 을 쓴다면 이제는 당연히 coroutine 을 쓸 것이고, 한 걸음 더 나아가면 반환 결과를 Result
로 받도록 만들고 싶다. 이건 retrofit-adapters 를 적용하면 되는데, 결과적으로 이런 모양이 된다.
@GET(...)
suspend fun fetchUser() : Result<UserDto>
json 매핑 - kotlinx serialization
API 호출을 하다보면 필연적으로 JSON 과 객체 매핑을 해야하는데, kotlinx serialization 을 쓰면 된다. kotlin 초창기엔 moshi 등이 언급되었으나, 지금 시점에 moshi 를 선택할 이유는 없다고 본다.
gson 쓰면 안되냐고 물어본다면 쓰지말라고 하고싶다. gson 의 경우엔 reflection 을 쓰기 때문에 kotlin 의 생성자 호출도 이뤄지지 않고, 이러다보면 몇가지 생각지 못한 문제가 생긴다. 예를 들어 다음과 같은 DTO 객체를 만들었다고 치자.
data class UserDto(
val first: String,
val last:String) {
val full: String = first + last
}
gson 으로 { "first": "a", "last": "b" }
를 매핑한 UserDto를 가져와서 full 변수의 내용을 보면 ab
가 아니라 null 이 들어있던가, 예외가 발생할거다. 생성자가 호출되지 않기 때문이다. 참고로 위의 내용은 좋은 예시는 아니긴 하다. 애초에 DTO 에 full 같은걸 넣지 말자.
영속적 데이터 저장 - jetpack datastore, room
쭉쭉 늘어나는 데이터라면 DB 기반의 room 을 , 그렇지 않다면 datastore 를 쓰면 된다.
datastore 는 shared preference 기반의 구현과 protobuffer 기반의 구현을 제공한다. protobuffer 가 좀 더 멋져보이긴 하지만, 나중에 migration 을 하다 골치아픈 일이 발생할 수도 있어서, 굳이 써야하나 싶다. 나라면 안쓰겠다.
DI - koin
koin 이 DI 냐, DI가 필요하냐 등의 많은 논쟁이 있는데 난 DI는 필요하고 생각하고, hilt 보단 koin 에 한표를 던진다. KMP 때문에 koin 이 더 힘을 받고 있기도 하고, dagger 시절부터 시작해서 hilt 로 내려온 이 복잡한 설정으로 늘어난 복잡도가 과연 필요한가 싶다. 내가 dagger 포기자이기도 하고(대포자...). 역시 반박 시 여긴 내 글이니 내 말이 맞...
koin 은 초기엔 런타임에 모든 설정이 이뤄지기 때문에 런타임에 크래시가 날 위험이 있긴 한데, annotation 부터 시작해서 최근엔 아직 베타이긴 하지만 intelliJ 플러그인 도 나와서 점점 이런 걱정들을 해소해주고 있다. 그리고 완전하진 않지만 단위테스트를 통해 검증하는 방법도 제공해 주고 있어, 쓰기 나름이라고 생각한다.
이미지 로더 - coil
이젠 옛 유물이 된 picasso 를 빼고나면, glide 와 coil 정도가 남지 않을까 싶은데 glide는 커스터마이징이 너무 어려워 이젠 그냥 coil 을 쓰는게 맞다고 생각한다.
UI - compose
이젠 완전히 compose 세상이 되었다. 하지만 지금도 뜻밖의 버그도 만나게 되고, 뭔가 미묘하게 문제가 발생해서 여전히 필요에 따라 군데군데 xml 기반의 뷰가 필요한 상황이 생긴다. 그래도 강력한 preview 덕분에 UI 개발이 훨씬 편해졌다.
View 와 ViewModel 간의 커뮤니케이션 - flow
MVVM , MVI 등 아키텍쳐 이야기는 너무 의견이 다양해서 생략하고, compose 덕분에 이제는 ViewModel 에서 상태를 StateFlow 등으로 노출하고, View 에선 이 상태를 구독한 후 composable 에 던져 적절히 렌더링하는 게 대세이다. 물론 여기에도 토스트 같은 side-effect 를 상태에 포함시킬거냐 등의 수많은 설계 고민거리가 들어갈 수 있겠다.
나는 mavericks 를 몇년간 써 오고 있긴 한데, 새로운 프로젝트라면 굳이 mavericks가 필요할까 싶다.
코드 품질 관리 - ktlint , konsist
여럿이 개발을 한다면 코딩 컨벤션 통일을 위해 ktlint 정도는 도입을 해 주는게 좋다. git precommit hook 등에 걸어두자.
써 보진 않았지만 프로젝트의 아키텍쳐 규칙 위반을 잡아내기 위한 konsist 도 좋아보인다.
단위 테스트 - junit 4
단위 테스트는 여전히 junit 4 기반으로 작성한다. robolectric 동작도 그렇고, 꼭 junit 5 여야 하는 이유도 크게 느끼지 못해서.
단위테스트 책을 읽은 후론 고전파 단위테스트 를 지향하려고 한다. 고전파를 지향하려면 최대한 mock 등을 쓰지 않고 실제 구현체를 제공해야 하는데, 만약 구현체가 다른 모듈의 internal class 라면 어떻게 제공하지? 하는 문제가 생긴다. 다행히 AGP 8.5 정도에서부터 kotlin 기반의 test fixture 기능을 제공하기 때문에, 적절히 test fixture 를 만들어두면 테스트 코드끼리 fixture 를 서로 공유할 수 있다. 이를 위해선 android.experimental.enableTestFixturesKotlinSupport=true
옵션을 build.gradle 에 넣어야하는데, 이 옵션에 대한 설명을 찾기가 정말 힘들다. android 개발 문서 사이트에서도 검색할 수 없고. 나는 대체 이걸 어디서 찾았지?
단위 테스트는 아니지만 그 외에 요즘은 스크린샷 테스트나 compose preview 기반의 테스트도 많이 이야기되고 있다. 이건 재밌어보여서 해보고싶다.
Top comments (0)