[Project] 아이모 잡학도구 2.1.0 정리

6 분 소요

본 앱은 구글 플레이스토어에 배포된 어플인 게임 ‘아이모’ 의 팬메이드 어플입니다. 2.1.0 업데이트를 수행하면서 공부한 내용을 정리하여 기록하고자 합니다.

변경 주요 이슈들은 다음과 같습니다.

  • Clean Architecture 적용 및 모듈화
  • DB 구조 변경
  • 지역화
  • 디자인 리소스 분류 및 다크테마 대응
  • 인앱결제

하나씩 정리해보고자 합니다.

Clean Architecture 적용 및 모듈화


클린 아키텍쳐는 구글 권장 아키텍쳐와 차이점이 있습니다.

구글 권장 아키텍쳐는 View > Domain(option) > Data 로의 의존성의 방향이 그려지게 되지만, 로버트 마틴의 클린아키텍쳐는 View > Domain < Data 의 방향을 가집니다.

즉, 도메인이 데이터를 아느냐? 모르느냐? 의 차이점이 있습니다.

사실 클린아키텍쳐 라는 것은 로버트 마틴이 생각하기에 좋은 패턴들을 모두 모아서 정의해놓은 것으로 이러한 구조를 가지는것이 클린아키텍쳐 라고는 볼수 없습니다.

하지만 이중에서의 가장 주된 핵심은 Domain이 Data를 모른다는 것인데요.

그럼 Domain 과 Data는 뭘까요?

이러한 구조는 유명한 Layered Architecture 에서 정의된 형태로 화면을 구성하고 UI로직을 담당하는 Presentation, 비즈니스 로직을 담당하는 Domain, 데이터로직을 담당하는 Data 로 3가지 Layer로 나누어 집니다.

화면의 구성과 화면에 보여지는 View들에 대한 이벤트들 혹은 이러한 화면이 어떻게 보여지고 그려져야 할지를 결정하는 로직들을 UI 로직 이라고 합니다.

비즈니스 로직은 프로젝트의 핵심 도메인들에 대한 기능들이 어떻게 구현되어 져야 하는지의 로직들을 말하고, 데이터 로직은 데이터를 어떻게 가져오고 가공할지의 로직들을 말합니다.

프로젝트의 핵심 도메인이라는 것은 제앱으로 예를들자면, 제앱에서 핵심적으로 제공하는 기능들은 몬스터의 알람설정, 몬스터의 드랍아이템 리스트, 아이템도감 리스트 인데요.

이들이 각각 도메인으로써 구성될수 있습니다. 또 이들은 안드로이드 플랫폼의 의존성을 가지지 않고 오로지 코틀린코드로 만 작성되어야 합니다.

그렇다면 Domain이 Data를 모른다는 것이 뭐가 다를까요?

모듈화

먼저 그보다 앞서서, 클린아키텍쳐를 적용하는 것은 모듈화 라는 것이 뒷받침 되어야 합니다.

모듈화 라는 것은 프로젝트의 기능들을 조각을 내어 하나의 독립적인 단일요소로 만들어 놓고 이를 조립하여 하나의 프로젝트를 구성시키는 것을 의미합니다.

이는 컴퓨팅 사고에서 하나의 큰 문제를 해결할수 있는 작은 문제들로 분해하여, 작은 문제들을 해결함으로써 복잡한 큰 문제를 해결할수 있다고 말하는 문제분해 와도 유사한데요.

모듈들은 모두 각기 수행하는 기능이 독립적으로 구성되고, App 모듈에서 이 모든 모듈들을 합쳐서 하나의 어플리케이션으로 빌드되어 집니다. 위의 문제분해와 같이 작은 모듈들을 만들어 나감으로써 하나의 앱을 만들수 있는거죠.

위에서 설명한 Data와 Domain 그리고 Presentation 도 모두 이러한 모듈들이고, 각기 정의된 기능들을 수행하는 독립적 단위 입니다.

특히, 안드로이드에서 모듈화 라는 것은 KMM(Kotlin Multiplatform Mobile) 또는 CMM(Compose Multiplatform Mobile)으로 앱을 구현하는데 있어서 가장큰 강점을 가집니다.

저 또한 업데이트 이전에 모듈화를 수행했던 가장 큰 이유가 KMM으로 Hybrid 앱으로 개발하여 IOS와 Desktop용 앱을 출시하는게 목적이었습니다만, 여러가지 이유들로 인해 모듈화만 적용하고 수행하지는 않았습니다.

모듈화라는 것이 어떻게 하이브리드앱을 개발하는데 중요할까요?

Dependency Injection

클린아키텍쳐의 로버트 마틴은 모듈간에 저수준이 아닌 고수준의 것을 의존하도록 구현하라고 말합니다.

저수준 이라는 것은 어떠한 기능에 대한 구체적인 것들을 말하고, 고수준 이라는 것은 기능에 대한 추상적인 것들을 말합니다.

이 구조는 DIP(Dependency Inversion Principle)를 적용하여 의존의 방향을 역전시켜 저수준의 모듈이 고수준의 모듈을 의존하도록 만들게 됨으로써 구현할수 있습니다.

클린아키텍쳐의 구조는 Domain인 유즈케이스를 중심으로 바깥의 Presentation과 Data 가 의존하는 형태입니다.

위에서 말한 대로 Data 가 Domain을 알지만 Domain은 Data를 모르게 됩니다.

이렇게 함으로써 Data가 Local DB인지, Datastore인지, 클라우드인지, 서버로 부터 remote 하는지를 Domain이 구체적으로 알지 못하게 됩니다.

즉, 열려있는 확장성을 갖게되어 우리는 앞으로 Local DB를 쓰지 않고 서버로부터 데이터를 받도록 변경한다고 했을 때 단순히 Data layer만 바꾸면 되어 비용이 줄어들게 됩니다.

마찬가지로 KMM으로 개발한다고 가정한다면 Data의 Local DB의 경우 안드로이드 플랫폼의 의존성을 갖기 때문에 이를 분리해두고, 서버로부터 remote 하는 부분은 Ktor 프레임워크를 이용하면 그대로 사용할수 있기 때문에 해당 모듈을 재사용 할수 있는 장점이 있습니다.

또한, Domain은 View를 알지 못하기 때문에 각 OS에 맞는 Presentation을 만들어 Domain 모듈과 연결하면 좀더 편리하게 하이브리드를 타겟팅하는 앱을 만들수 있습니다.

결국 정리하자면 도메인 중심 개발에 적합한 클린 아키텍쳐의 구조로 좀더 변경과 확장에 자유로운 개발을 할수 있어서 저의 프로젝트에 적용을 해보았었습니다.

하지만, 앱의 이용자수가 크지 않다는 점과 실질적으로 KMM으로 전환하기 위해 안드로이드 플랫폼에 의존성이 있는 부분들을 모두 변경하여야 한다는 점, 최근에 Stable 버전이 출시한다고는 하지만 여전히 IOS를 타겟팅으로 하는 부분에서 비동기 처리의 구현과 디버깅의 어려운점들

뿐만아니라 핵심적으로 IOS는 앱출시를 위해 개발자 등록을 연100$ 를 지불해야 하는데 그만큼의 수익을 얻어낼 가능성이 없다는점 에서 모듈화 이후 KMM으로 전환하지는 않았습니다.

결과적으로, 아이모 잡학도구 프로젝트의 초기 버전의 스파게티코드 형태일 때는 학업에 따라가느라 방치해두고 방학에 다시 마주했을 때 제코드를 이해하지 못했었지만, 이제는 적용된 아키텍쳐와 디자인패턴으로 가독성이 늘어나 추후 유지보수하는데 어려움이 없을 것 같습니다.

DB 구조 변경


기존에는 Collection 파트의 Book 릴레이션에 스텟값들을 여러 속성으로 두어 관리했었습니다.

Collection_ver1

하지만, 최근 아이모의 도감이 업데이트 되면서 없던 스텟들이 추가되기 시작했고 뿐만아니라 기존 구조에서는 불필요한 저장공간을 낭비한다는 문제점이 있었습니다.

스텟값들은 28가지로 굉장히 많은데, 이에 대해 Book 릴레이션에서 각각의 튜플들이 모든 스텟값을 가지지 않기 때문에 0의 값을 가져야만 했습니다.(Not null로 관리했기 때문입니다.)

또한, 스텟속성을 추가하려면 해당 스텟을 가지는 일부 튜플들을 제외하고 모든 튜플들에 0값을 또 넣어주어야 함으로써 결국 저장공간이 낭비된다는 문제점이 있었습니다.

위 구조가 만들어진 이유는 도메인을 중심으로가 아닌 데이터를 중심으로 생각해서 만들었기 때문입니다.

Collection 이라는 것은 어떠한 아이템들을 특정 개수만큼 지불했을 때 특정 스텟들을 받을수 있는 시스템입니다.

기존에 데이터 관점으로 바라보아 이러한 정해져 있는 데이터들로 Entity를 구성한뒤에 가공을 시키다보니 데이터에 맞는 Entity를 만들게 됨으로써 불필요하게 설계됬던것 같습니다.

이를 데이터 관점이 아닌 도메인 관점으로 바라보아, 앱에서 수행되는 아이템 도감(Collection) 조회 기능은 ‘특정 아이템들로 이러한 스텟들을 준다’ 라고 설정하고, Book 릴레이션 구조를 변경하였습니다.

Collection_ver2

데이터는 어떤 스텟값들이 있는지는 중요하지 않고, 특정 Book의 튜플에서 가져야할 스텟값들이 무엇인지로 표현하기 위해서 Stat 개체를 약성개체로 표현하고, 명칭과 값을 속성으로 두었습니다.

즉, 스텟들은 Book 이 존재하지 않으면 존재할수 없으며, 하나의 Book 튜플이 여러 스텟값을 가지기 때문에 ‘능력치 상승’ 이라는 약성 관계로 분류하여 표현했습니다.

이를 통해 도메인에서 enum class로 스텟들을 관리하고, 만약 추가되는 스텟속성이 있다면 여기에 추가하고 관련 데이터를 DB에 집어넣기만 하면 됩니다.

지역화


앱의 View 영역에 존재하는 string 리소스들을 기존에는 하드코딩이 되어 있었습니다.

한국 마켓만 타겟팅 한다면 이러한 행위는 딱히 문제가 된다고 생각하지 않습니다만, 지역화를 통해 타국 마켓에 출시한다면 이들을 모두 string 리소스로 정리해두어야 합니다.

앱의 values 폴더내에 특정 국가에 대한 locale 로 정의된 directory를 먼저 생성하고, 그곳에 string.xml을 만들어서 정의된 언어에맞게 default string.xml의 문자들을 오버라이딩 해주면 됩니다.

화면영역의 지역화는 어려운 내용은 아닙니다만, 여기서 체계화가 조금 필요합니다.

예를들면 확인 이라는 단어를 지역화한다면 이 단어는 보통 확인버튼이 필요한 모든 화면들에서 할당될겁니다. 1인개발이라면 내가 확인 이라는 단어를 만들어 둔건지 아닌지 알수있지만, 다수의 개발자와 협업한다면 이를 미리 약속해두지 않으면 중복해서 만드는 문제가 생길수 있습니다.

따라서 개발자간에 string resource에 대한 약속을 디자인파트와 협업하여 미리 정해두는 형태로 약속해두는 과정이 꼭 필요할것 같습니다.

저는 화면영역 뿐만아니라 앱내의 local DB에서 받아오는 데이터도 지역화가 필요했습니다.

이를 위한 2가지 방법이 떠올랐는데, 하나는 문자열 속성들이 있는 릴레이션에 번역속성을 하나더 만들어서 추가하는 방법과 다른하나는 특정 국가를 타겟팅하는 DB를 새로 만드는 것이었습니다.

만약 전자로 하게 된다면, 앱의 DB구조가 변경되기 때문에 migration 에대한 코드를 room에 정의해주어야만 하고, 후자로 한다면 앱의 root locale에 따라 db만 분기해주면 될것 같았습니다.

migration의 경우 코드가 조금이라도 틀리면 ANR을 맞이할 수 있기 때문에 테스트코드를 통한 충분한 테스팅이 수행되어야만 합니다.

저는 현재 코드에서 room의 schema를 export 해둔 상태가 아니였어서 version history를 갖지 않아 migration를 해줄수가 없었고, user에게 재설치를 요구하는 UX를 기존에 제공했을 때 유저이탈이 굉장히 컷기 때문에 이방법 대신 후자를 선택하면서 현재버전을 업데이트 할 때 export=true로 바꾸어 배포하였습니다.

하지만 의도와는 달리 기존에 설치된 유저가 DB에 DML을 수행한적이 있다면 locale에 따라 DB의 instance에 createFromAsset() 이 분기되지 않는 현상이 있었습니다.

찾아보니 createFromAsset()을 수행하면 asset의 DB를 복사하여 만들어진 DB파일을 가지고 cache 해두기 때문에, 이전에 cache해둔 DB를 그대로 사용하여 locale에 따라 분기가 되지 않았습니다.

즉, 앱내에서 사용자의 요구에 의해 언어를 바꾸고자 한다면 DB 내부에서 번역된 string을 가지고 있어야 할것 같습니다만 지역화 자체를 지원하는 이유는 해외마켓에 출시하기 위함이었기 때문에 특별한 문제가 되지는 않아 그대로 출시하였습니다.

향후 이부분에 대해서 유저들의 요구가 있다면 변경하여야 할것 같습니다.

디자인 리소스 분류 및 다크테마 대응


디자인 리소스들은 현재 design-ui 와 design-compose 모듈로 분리하여 관리해둔 상태입니다.

각 feature 들은 이들을 의존하여 분류된 디자인요소들을 가지고 있고, design-ui의 경우 android view에서 가지는 color, theme 과 같은 요소들을 포함하고 있고 design-compose 는 compose 와 관련된 요소들을 포함하고 있습니다.

특히, color resource 들을 material design 에서 정의된 형태로 최대한 정의해두고 각 theme 에서 이를 이용하는 형태로 두어 다크테마 까지 대응하였지만,

Material design에서 구체적으로 어떤게 background고 surface이고, error이고 등등을 명확하게 정의해 두지 않아 조금 혼동이 생겼지만 최대한 맞춰서 하려고 노력했습니다.

인앱결제


유저들에게 앱내에서 알람 설정시 발생하는 리워드 광고를 제거할 수 있는 인앱결제를 구현해달라는 요구가 많았습니다.

그래서 인앱결제를 위한 BillingModule 클래스를 정의하고 해당 객체로 인앱결제의 lifecycle을 관리하도록 하였습니다.

인앱결제의 경우 중요한 부분은 앱 내에서 결제를 요청한 후 실제 결제가 완료되기 위해 구매처리 과정을 반드시 수행시켜 주어야 한다는 점입니다.

이를 구글에서는 onResume() 내에서 처리하는 것을 권고하고 있습니다. 앱에서 결제를 받기위해 결제창을 띄우면 앱위에 결제창이 별도의 앱으로 띄워지는 형태기 때문에 제앱이 onStop()이 호출되고 결제를 취소하든 완료하든 제앱으로 재진입하게 되면서 onStart() - onResume()이 호출되게 됩니다.

이 때에 user의 구매내역을 가져와서 처리되지 않은 구매의 처리를 수행해 주어야 합니다.

그외에는 인앱결제 공식문서 를 참조하여 진행하면 됩니다.

태그:

카테고리:

업데이트:

댓글남기기