Android 온스타일 레거시 코드에 클린 아키텍처 도입기

부제 : Mapper의 재발견

신규찬
CJ 온스타일 기술 블로그
12 min readMay 5, 2024

--

안녕하세요.
CJ ENM 커머스 부문에서 Android 앱을 개발하는 신규찬입니다.

CJ온스타일의 홈 탭을 개편하며 기존의 레거시 코드를 개선하고 클린 아키텍처를 도입하는 과정을 통해 많은 변화와 발전을 이뤄냈습니다. 특히, 이번 프로젝트에서 두드러진 성과 중 하나는 Mapper 기능 도입입니다. 이 글에서는 이러한 개선 과정과 Mapper 기능의 역할 및 그로 인한 효과를 소개 드립니다.

대규모 시스템에서 클린 아키텍처를 적용하는 것은 어려운 과제일 수 있습니다. 온스타일도 마찬가지였습니다. 깊은 레거시 코드가 존재하는 상황에서 클린 아키텍처를 도입하려는 과정에 많은 고민이 있었습니다.

그러던 중에 온스타일은 홈탭 개편 프로젝트를 진행하게 되었습니다. 이는 기존 시스템의 중요한 부분을 대대적으로 수정하고자 하는 대규모 프로젝트였습니다. 우리는 이를 통해 클린 아키텍처를 적용할 수 있다면, 충분한 테스트를 거친 뒤 안정적으로 프로젝트를 오픈할 수 있을 것으로 기대했습니다.

새로워진 온스타일 홈탭

클린 아키텍처 적용 전 고려사항

온스타일 레거시 코드에 클린아키텍처를 적용할 때 주요 고려사항은 아래와 같습니다.

  1. 분석과 계획: 우선적으로 레거시 코드의 현 상태를 분석하고, 클린 아키텍처를 어떻게 적용할지 계획을 세웠습니다. 어떤 부분을 수정해야 할지, 어떤 모듈을 우선적으로 개선해야 할지를 결정하는 데 많은 시간을 할애했습니다.
  2. 모듈화와 리팩토링: 레거시 코드를 모듈화하고, 이를 클린 아키텍처의 원칙에 맞게 리팩토링했습니다. 각 모듈은 단일한 책임을 가지도록 분리되었고, 각 모듈 간의 의존성을 최소화하고자 했습니다.
  3. 테스트와 검증: 클린 아키텍처를 적용한 후에는 충분한 테스트를 진행하여 안정성을 확보했습니다. 내부 테스트와 QA 테스트를 반복하여 신뢰할 수 있는 시스템을 만들기 위해 노력했습니다.

이렇게 온스타일은 홈탭 개편 프로젝트를 통해 레거시 코드에 클린 아키텍처를 성공적으로 적용하였습니다. 이는 대규모 프로젝트에서도 클린 아키텍처를 적용할 수 있는 가능성을 보여주었습니다.

레거시코드에 클린 아키텍처 놓이기

레거시 코드로 작성된 Tab Fragment는 UI, API, 그리고 비즈니스 로직이 한 곳에 집중되어 깊은 의존성을 가지고 있었습니다. 이번 홈탭 개편 작업에서는 기존의 비즈니스 로직을 유지하면서, API와 UI는 새롭게 변경해야 하는 과제가 주어졌습니다.

통신 부분의 분리

레거시 코드에서 통신 부분을 분리하는 가장 간단한 방법은 상속을 통한 통신 부분의 분리입니다.

레거시 코드 상속을 통한 클린 아키텍처 구성 UML

홈 탭의 API 호출을 담당하는 함수를 오버라이드하여 클린 아키텍처 구조에 맞게 통신을 진행했습니다. 이를 통해 기존 구조가 RxJava를 사용하여 비동기적으로 데이터 통신 값을 가져와 UI에 직접 입력하던 방식에서, 코루틴을 활용하여 데이터를 가져오는 코드로 변경할 수 있었습니다.

홈 탭의 구조상 API에서 받은 데이터를 특정 상품 규격에 맞게 변경해야 했습니다. 이전에는 Tab Fragment에서 데이터 파싱 작업과 데이터 구성을 담당했으며, 이로 인해 비즈니스 로직이 복잡해지고 소스 코드의 양이 증가하여 가독성이 저하되었습니다. 이번 개선 작업에서는 데이터 파싱 및 비즈니스 로직 작업을 클린 아키텍처 구조의 도메인 데이터 레이어를 통해 API 통신과 비즈니스 로직을 처리하는 형태로 진행했습니다. 이 구조를 통해 다양한 데이터 구조를 매퍼(Mapper)를 사용하여 파싱할 수 있었고, 필터를 통해 비즈니스 로직을 적용할 수 있었습니다.

Mapper 의 기능 활성화

이번 프로젝트에서 가장 큰 성과는 Mapper의 도입입니다.

Mapper는 API 에서 내려준 DTO(Data Transfer Object)를 UI에 적합한 Entity로 변경하는 기능을 담당하는데, 이번 홈탭 개편에서의 Mapper는 다양한 역할을 하였습니다.

다양한 데이터 구조 파싱

홈 탭은 개인화, 상품, 랭킹 등 다양한 데이터를 제공하며, 이 데이터를 상품 특성에 맞춰 파싱하고 리스트로 표현하는 작업은 개발자의 몫입니다. 레거시 코드에서는 이러한 파싱 작업이 주로 어댑터나 프래그먼트에서 이루어졌으며, 비즈니스 로직과 UI 로직이 혼재되어 코드 양이 많고 가독성이 낮았습니다.

Json 을 Mapper 를 통한 상품 List 구성

이번 홈 탭 개편에서는 클린 아키텍처의 매퍼 기능을 도입하여 다양한 데이터의 파싱 작업을 처리했습니다. 각 상품 유형에 맞춘 매퍼 구성을 통해 프레젠테이션, 도메인, 데이터 레이어 간의 역할 분리를 명확히 하여 코드의 가독성을 높였습니다. 이러한 구조적 개선은 데이터 처리의 효율성을 향상시키고, 유지보수를 용이하게 만듭니다.

class ItemListMapper(val parameter: Parameter?) : BaseMapper<IteListDto, ItemListEntity>() {
override fun convert(from: IteListDto): ItemListEntity {
val itemList = java.util.ArrayList<Item>()
...

val jsonArray = from.itemList as? JSONArray
jsonArray?.let {
itemList.addAll(parseItem(jsonArray))
}

...
}

fun parseItem(itemArray: JSONArray): ArrayList<Item> {
val itemList = java.util.ArrayList<Item>()

val length: Int = itemArray.length()
for (index in 0 until length) {
try {
...

// 비즈니스 로직
// 조건에 만족하지 않았을 경우 skip
val expires = itemArray.getString.getString("expires")
val itemTypeCode = itemArray.getString.getString("itemTypeCode")
if(!checkValid(expires)) continue

when (itemTypeCode) {
ItemTypeConstants.ITEM_TYPE_A.tpCd -> ItemTypeAMapper()
ItemTypeConstants.ITEM_TYPE_B.tpCd -> ItemTypeBMapper()
ItemTypeConstants.ITEM_TYPE_C.tpCd -> ItemTypeCMapper()
ItemTypeConstants.ITEM_TYPE_D.tpCd -> ItemTypeDMapper()
ItemTypeConstants.ITEM_TYPE_E.tpCd -> ItemTypeEMapper()
else -> null
}?.let {
...
}
} catch (e: Exception) {
e.printStackTrace()
//에러 나기 전까지의 데이터를 넘겨준다.
return itemList
}
}

return itemList
}

Mock Data 환경 구성

모든 개발 프로젝트에서 디자인과 API가 완성된 후 앱 개발이 순차적으로 이루어지기를 기대하는 것은 현실적으로 어렵습니다. 개발 기간이 정해져 있고, 디자인과 API가 완성되기를 기다리는 것에는 한계가 있습니다. 일반적으로 개발 회사들은 초기 단계에서 Mock API를 제공하고, 이후에 실제 API 개발이 완료되면 이를 통합하는 2 Step 접근법을 사용합니다.

이번 홈탭 프로젝트도 Mock API를 먼저 제공받아 개발을 시작했습니다. 보통 Mock 데이터의 일부만 변경하고자 할 때, 모든 Mock 데이터에서 필요한 부분을 찾아 수정해야 하는데, 이는 JSON 오타를 유발할 수 있으며, 많은 데이터 중 원하는 특정 상품의 데이터만 변경하는 것은 어려움이 있습니다.

그러나 이번 홈탭에서는 매퍼를 통한 파싱 작업이 있어, 각 상품 유형별로 파싱을 할 수 있는 구조를 마련했습니다. 따라서 변경을 원하는 상품 유형의 JSON만 수정하여 원하는 Mock 데이터를 설정할 수 있었습니다. 이 구조 덕분에 변경하는 위치가 중간이든 맨 끝이든 상관 없이 오류 없이 빠르게 Mock 데이터를 수정할 수 있었습니다.

class ItemTypeAMapper : ItemMapper<String, ArrayList<ItemTypeAContentEntity>, ArrayList<Item>?>() {

override fun convert(json: String?): ArrayList<Item>? {
...

// Mock Data 넣어줌
val jsonStr = mock()
val arrayList = ArrayList<Item>()
arrayList.add(ItemTypeAEntity().apply {
contentList = Gson().fromJson<ArrayList<ItemTypeAContentEntity>>(jsonStr, object : TypeToken<ArrayList<ItemTypeAEntity>>() {}.type)
})
return arrayList
}

/**
* Mock Json 을 넣어 API 통신 없이 데이터 구성
*/
private fun mock(): String {
return "[\n" +
" {\n" +
" \"id\": 1,\n" +
" \"name\": \"테스트 상품 \",\n" +
" \"dpStrDtm\": \"2024-05-01T00:00:00\",\n" +
" \"dpEndDtm\": \"2024-05-31T23:59:59\",\n" +
" \"price\": 100000\n" +
" }\n" +
"]"
}

디버깅 환경 구성

과거에는 특정 상품 유형만 디버깅하기 위해 RecyclerView 어댑터에서 해당 유형을 제외하기 위해서 주석 처리하거나, 적절한 Mock 데이터를 준비하는 등의 어려움이 있었습니다. 이번 홈 탭 개발에서는 매퍼를 통해 파싱 시 원하는 상품 유형만 선택적으로 파싱할 수 있게 구성할 수 있었으며, 디버깅을 위한 Mock 데이터도 손쉽게 입력할 수 있었습니다. 이러한 디버깅 환경은 매퍼에서만 처리함으로써 설정을 빠르게 완료할 수 있었습니다. 또한, 이 방법을 동료들과 공유함으로써 팀 전체의 개발 속도도 향상시킬 수 있었습니다. 이는 개발 효율성을 높이는 동시에 동료들 간의 협업을 원활하게 만드는 접근 방식이었습니다.

class ItemListMapper(val parameter: Parameter?) : BaseMapper<ItemListDto, ItemListEntity>() {
...

fun parseItem(itemArray: JSONArray): ArrayList<Item> {
...

for (index in 0 until length) {
try {
...

when (itemTypeCode) {
ItemTypeConstants.ITEM_TYPE_A.tpCd -> ItemTypeAMapper()
ItemTypeConstants.ITEM_TYPE_B.tpCd -> ItemTypeBMapper()

// ==== 주석으로 해당 ItemType은 매핑 시키지 않는 테스트 진행
// ItemTypeConstants.ITEM_TYPE_C.tpCd -> ItemTypeCMapper()
// ItemTypeConstants.ITEM_TYPE_D.tpCd -> ItemTypeDMapper()

// === ItemTypeEMapper 을 DebugTypeEMapper 로 변경 후 테스트 진행
ItemTypeConstants.ITEM_TYPE_E.tpCd -> DebugTypeEMapper()
else -> null
}?.let {
...
}
} catch (e: Exception) {
...
}
}

return itemList
}

마무리

홈탭 개편은 ‘온스타일’ 앱의 대문을 새롭게 디자인하는 중요한 작업으로, 많은 분들의 관심과 응원으로 개발이 진행되었습니다. 이 과정에서 레거시 코드를 현대적인 기반으로 변모시킬 수 있는 토대를 마련하였으며, 이를 성공적으로 수행하였습니다. 특히, 클린 아키텍처 구조를 도입함으로써 기존보다 더 자유롭게 테스트 환경을 구성할 수 있었고, 코드의 가독성도 크게 향상시켰습니다.

홈탭의 이번 개편과 클린 아키텍처의 도입은 단지 시작일 뿐입니다. 앞으로도 사용자들에게 더 나은 사용 경험을 제공하고, 그들을 감동시킬 수 있는 방법을 지속적으로 모색하며 ‘온스타일’을 개선해 나갈 것입니다.

--

--