요즘IT
위시켓
최근 검색어
전체 삭제
최근 검색어가 없습니다.

국내 IT 기업은 한국을 넘어 세계를 무대로 할 정도로 뛰어난 기술과 아이디어를 자랑합니다. 이들은 기업 블로그를 통해 이러한 정보를 공개하고 있습니다. 요즘IT는 각 기업의 특색 있고 유익한 콘텐츠를 소개하는 시리즈를 준비했습니다. 이들은 어떻게 사고하고, 어떤 방식으로 일하고 있을까요?

회원가입을 하면 원하는 문장을
저장할 수 있어요!

다음

회원가입을 하면
성장에 도움이 되는 콘텐츠를
스크랩할 수 있어요!

확인

개발

CJ온스타일의 안드로이드 앱 '클린 아키텍처' 도입기

년차,
어떤 스킬
,
어떤 직무
독자들이 봤을까요?
어떤 독자들이 봤는지 궁금하다면?
로그인

국내 IT 기업은 한국을 넘어 세계를 무대로 할 정도로 뛰어난 기술과 아이디어를 자랑합니다. 이들은 기업 블로그를 통해 이러한 정보를 공개하고 있습니다. 요즘IT는 각 기업의 특색 있고 유익한 콘텐츠를 소개하는 시리즈를 준비했습니다. 이들은 어떻게 사고하고, 어떤 방식으로 일하고 있을까요?

 

이번 글에서는 CJ온스타일 안드로이드 개발자가 안드로이드 신규 프로젝트에 클린 아키텍처를 도입한 이유와 이를 통해 개발자의 피로도를 감소시킨 경험을 공유합니다. 

 

소프트웨어 개발은 복잡한 문제를 해결하기 위해 코드를 작성하는 것 이상을 필요로 합니다. 단순히 결과론으로 화면을 보여주는 것만 생각하고 개발한다면, 코드는 점차 커지며 나중에는 손볼 수 없을 정도로 난폭한(?) 코드로 변하게 됩니다.

 

그렇기에 개발한다는 것은 유지보수성, 확장성, 테스트 용이성 등의 요구 사항을 충족하기 위한 고민이 필요하며, 적절한 아키텍처 설계가 필수입니다. 이러한 목표를 달성하기 위한 중요한 개념 중 하나가 클린 아키텍처입니다.

 

셀렙샵 APP 구축 프로젝트를 진행하며 클린 아키텍처를 도입한 이유와 도입 후 각 계층(Layer)의 구성 요소 모두 구현하기 위해 증가한 개발자의 피로도를 해결한 방법에 대해서 소개해 드리겠습니다.

 

클린 아키텍처란?

클린 아키텍처는 소프트웨어 시스템의 구조를 설계할 때 지켜야 할 원칙과 방법을 정의한 개념입니다. 이 개념은 로버트 C. 마틴(Robert C. Martin)에 의해 소개되었으며, 복잡한 소프트웨어 시스템을 보다 관리 가능하고 유지보수 가능한 형태로 구축하기 위한 지침을 제공합니다.

 

이 아키텍처는 소프트웨어의 유지보수성, 테스트 용이성 및 모듈 간의 분리를 강조하여 안드로이드 애플리케이션을 더 구조화된 방식으로 개발할 수 있도록 돕습니다.

 

 

왜 클린 아키텍처가 필요한가?

소프트웨어가 가진 본연의 목적을 추구하려면 소프트웨어는 반드시 ‘부드러워’야 한다. 다시 말해 변경하기 쉬워야 한다. 이해관계자가 기능에 대한 생각을 바꾸면, 이러한 변경사항을 쉽게 적용할 수 있어야 한다.
(로버트 C. 마틴, 클린 아키텍처)

 

<출처: The Clean Code Blog>

 

클린 아키텍처는 소프트웨어 시스템의 구조를 체계적으로 설계함으로써 여러 가지 장점을 제공합니다.

 

가장 중요한 장점 중 하나는 시스템의 각 부분을 독립적으로 개발하고 테스트할 수 있는 환경을 조성할 수 있다는 점입니다. 또한 시스템의 변경이나 업그레이드가 필요할 때 전체 시스템을 다시 작성하지 않아도 되며, 특정 부분만 수정하는 것이 가능해집니다.

 

예를 들어보면 Network 통신 라이브러리인 Retrofit의 ConvertFactory를 Moshi Factory로 사용하는 프로젝트가 있다고 가정하겠습니다. 어느 날 Moshi Facory가 아닌 GsonconvertFactory 으로 전면 변경하라는 기술 지침이 내려온다면 개발자는 프로젝트 안에서 Network 통신하는 곳을 모두 찾아서 Moshi Factory 를 GsonconvertFactory 라이브러리로 변경하는 귀찮음과 코드를 찾는 수고가 발생할 수 있습니다.

 

만약 클린 아키텍처로 변경되었다면 어떻게 될까요? Network 통신을 담당하는 Layer 만 확인 후 Network 통신 라이브러리를 변경작업하면 됩니다. 이렇게 된다면, 휴먼 에러로 라이브러리를 변경 처리를 놓치는 케이스도 없을 것입니다.

 

 

주요 원칙

클린 아키텍처의 주요 원칙은 다음과 같습니다.

 

  • 의존성 역전 원칙(Dependency Inversion Principle)
    고수준 모듈은 저수준 모듈에 의존해서는 안되며, 양쪽 모듈 모두 추상화에 의존해야 합니다. 이를 통해 느슨한 결합을 유지할 수 있습니다.
  • 경계(Boundary)의 분리
    시스템을 여러 영역으로 나누고, 각 영역 사이의 인터페이스를 정의하여 각 영역의 독립성을 보장합니다.
  • 인터페이스 분리 원칙(Interface Segregation Principle)
    클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않아야 합니다. 즉, 인터페이스는 클라이언트의 요구에 딱 맞는 형태로 분리되어야 합니다.

 

 

클린 아키텍처 계층

클린 아키텍처는 다양한 계층(Layer)으로 구성되며, 각 계층은 특정한 역할과 책임을 가지고 있습니다.

클린 아키텍처의 주요 구성 요소는 다음과 같습니다:

 

  • 프레젠터(Presenter) 또는 뷰모델(ViewModel)
    사용자 인터페이스(UI)와 비즈니스 로직 간의 중간 계층으로, UI에서 발생한 이벤트를 처리하고 필요한 데이터를 비즈니스 로직에 전달합니다.
  • 유스케이스(Use Case)
    애플리케이션의 실제 비즈니스 로직을 포함하는 부분으로, 프레젠터나 뷰모델로부터 전달된 요청을 처리하고 데이터를 가공하여 반환합니다.
  • 리포지토리(Repository)
    데이터베이스나 외부 데이터 원본과의 상호 작용을 담당하는 부분으로, 데이터를 가져오고 저장하는 작업을 수행합니다. 유스케이스는 리포지토리를 통해 데이터를 얻어옵니다.
  • 엔티티(Entity)
    애플리케이션의 핵심 데이터 구조를 나타내며, 데이터베이스나 네트워크에서 가져온 데이터를 객체로 변환한 형태입니다.

 

 

클린 아키텍처의 장점은 무엇일까?

클린 아키텍처의 주요 장점은 다음과 같습니다.

 

  • 유지보수 용이성
    각 계층이 분리되어 있기 때문에 한 계층을 변경해도 다른 계층에 영향을 미치지 않아 유지보수가 쉽습니다.
  • 테스트 용이성
    의존성을 주입하여 유닛 테스트 및 통합 테스트를 수행하기 용이합니다.
  • 모듈 간의 분리
    각 계층이 자체 역할을 가지며, 이로 인해 코드의 재사용성이 높아집니다.
  • 데이터베이스나 UI 프레임워크 변경 용이성
    중요한 비즈니스 로직은 외부 프레임워크와 분리되어 있어 해당 프레임워크를 변경하더라도 기존에 작성한 전체 코드를 다시 작성할 필요가 없습니다.

 

 

셀렙샵 프로젝트 X 클린 아키텍처

저희 팀은 최근 셀렙샵 APP 구축 프로젝트에서 클린 아키텍처를 적용하여 많은 이점을 얻었습니다.

 

기능 구현에 있어서 다수의 개발자가 독립적으로 개발할 수 있었으며, 의존성이 낮아 개발한 소스에 대한 영향을 미치지 않는 장점을 경험했습니다. 또한 계층(Layer)의 독립적으로 테스트할 수 있는 환경을 구축하여 품질 유지도 이전 프로젝트에 비해 향상되었습니다.

 

 

클린 아키텍처를 왜 도입하였나?

기존 프로젝트는 MVVM(Model-View-ViewModel) 아키텍처로 구성되어 있습니다. 개발자가 많아진 만큼 프로젝트가 커지며 관리측면에서 어려움을 경험하였습니다.

 

MVVM 아키텍처 적용한 프로젝트 구조도

 

의존성을 최소화하여 독립적인 구조를 고민하였고, 이미 많은 빅테크회사에서 사용하고 있는 Clean Architecture 도입을 검토하였습니다.

 

  • 유지 보수성 향상
    Clean Architecture는 의존성 관리 및 모듈화를 통해 애플리케이션의 유지 보수성이 MVVM 보다 향상됩니다. 각 계층(Layer)은 명확하게 정의되고 분리되어 있으므로 변경 요구사항에 맞춰 변경이 쉽게 이루어질 수 있습니다.
  • 테스트 용이성
    유지 보수성 향상과 동일 이유로 각 계층이 독립적으로 테스트 가능하도록 설계되어 있어 테스트 용이성을 높일 수 있습니다. 유닛 테스트, 통합 테스트 등을 보다 쉽게 테스트를 진행할 수 있습니다.
  • 미래 확장성
    새로운 기능을 추가하거나 기존 기능을 변경하려는 경우 변경의 범위를 최소화하며 애플리케이션을 확장 유용합니다. MVVM은 뷰와 뷰 모델 간의 관계에 중점을 두는 반면 Clean Architecture는 시스템의 전반적인 구조를 고려합니다.
  • 비즈니스 로직 분리
    비즈니스 로직을 다른 계층으로 분리하여 비즈니스 룰과 데이터베이스 접근을 분리시킵니다. 이것은 비즈니스 로직을 단일 위치에서 유지하고 이해하기 쉽게 만듭니다. MVVM도 비즈니스 로직을 ViewModel에서 처리하여 독립적으로 운영할 수 있습니다. 하지만 프로젝트가 커짐에 따라 비즈니스 로직을 ViewModel에서 관리하기에는 어려울 수 있습니다.

 

위 4가지 측면과 함께 프로젝트 확장성을 고려했을 때 클린 아키텍처의 도입을 주저할 이유가 없었습니다.

 

 

클린 아키텍처를 적용하며

클린 아키텍처를 Android 의 구조에 맞춰서 개발하는 과정은 나침판을 들고 향해하는 기분이었습니다. 그 이유는 이미 Google Developer Document(Guilde to app architecture) 에서 Android 의 클린아키텍처에 대해 기술되어 있으며, 다수의 블로그에서 클린아키텍처 적용에 대해서 다뤄지고 있습니다.

 

저희 팀은 클린 아키텍처 도입을 하면서 View에 대한 비즈니스 로직을 View Model에서 관리하고 싶었습니다. 그렇게 하기 위해서는 MVVM 아키텍처 개념을 일부 가져오는 방법에 대해서 고민이 필요했습니다.

 

클린 아키텍처 + MVVM 아키텍처에 대한 아키텍처 구조

 

그 결과 클린 아키텍처 계층(Layer) 중 Presentation Layer 를 MVVM의 View 와 ViewModel로 정의하였고, Domain 계층(Layer) 과 Data 계층(Layer)을 MVVM의 Model로 정의하였습니다.

 

표로 간략하게 표현하면 다음과 같습니다.

 

 

Android 개발 시 클린 아키텍처의 각 계층(Layer)는 모듈(Module) 로 구성하였습니다. 계층(Layer)을 모듈로 구성한 이유는 모듈의 다양한 이점을 활용하기 위해 선택한 것입니다. 이러한 이점으로는 코드의 재사용성, 유지보수 용이성, 병렬 개발 가능성, 가독성 향상, 레거시 코드 관리의 효율성, 그리고 전반적인 개발 속도 향상 등이 있습니다.

 

셀렙샵 모듈(Module)

 

 

클린 아키텍처 구성요소 연결

클린 아키텍처에서 계층(Layer)과 구성 요소 간의 연결은 인터페이스를 활용하여 구성되며, 다른 구성 요소 간의 의존성은 의존성 주입(DI)을 통해 최소화하였습니다.

 

예를 들어 Best Item 을 가져오는 UseCase 을 구현한다고 가정해 보겠습니다.

BestItemUseCase 는 Domain 모듈(Module)에 위치하게 되며 이 UseCase는 동일 위치(Domain)에 있는 Repository의 getBestItems() 을 호출하게 됩니다.

 

class BestItemUseCase(
    private val repository: BestItemRepository
) : BaseUseCase<String, BestItemEntity>() {
    override suspend fun execute(parameters: String?): Result<BestItemEntity> {
        return 
	  // todo.. result 성공, 실패에 대한 처리 로직 추가
        }
    }
}

 

이때 정의한 BestItemRepository는 interface로 구성됩니다.

 

interface BestItemRepository {
    suspend fun getBestItemItems(): Result<BestItemEntity>
}

 

BestItemUseCase의 Repository는 DI로 정의하여 @Provides 를 구성하여야 합니다. API 통신은 Activity의 Lifecycle을 따르도록 DI Component를 ActivityRetainedComponent 로 정의하였습니다.

 

@Module
@InstallIn(ActivityRetainedComponent::class)
class BestItemUseCaseModule{
    @Provides
    fun provideBestItemUseCase(repository: BestItemRepository) = BestItemUseCase(repository)
}

 

BestItemRepository 의 implement 처리는 Data 모듈(Module) 에서 처리하게 됩니다. Repository는 DataSource를 호출하게 되며 DI로 Repository와 Datasource의 의존을 구성합니다.

 

class BestItemRepositoryImpl @Inject constructor(
    private val remote: BestItemRemoteDataSource
) : BestItemRepository {
    override suspend fun getBestItems(): Result<BestItemEntity> {
        val response = remote.getBestItemItems()
        // todo.. response에 대한 처리 로직 추가
    }
}

 

Repository 에 대한 @Provides 의 정의는 Data 모듈(Module) 에 구성되어 있으며, UseCase와 동일하게 DI Component를 ActivityRetainedComponent 로 정의하였습니다.

 

@Module
@InstallIn(ActivityRetainedComponent::class)
class BestItemRepositoryModule {
    @Provides
    fun provideBestItemRepository(
        remote: BestItemRemoteDataSource
    ): BestItemRepository = BestItemRepositoryImpl(remote)
}

 

도메인 계층(Layer)과 데이터 계층(Layer) 간의 연결을 DI(Dependency Injection)를 활용하여 설정함으로써, 의존성을 적절히 관리하고 클린 아키텍처를 구성할 수 있습니다.

 

 

API 에 따른 ConvertFactory 설정 변경

클린 아키텍처의 장점은 유연한 소프트웨어를 제공하는 것입니다. 유연한 소프트웨어를 적용 사례로 API Response에 따른 Retrofit ConvertFactory 변경을 소개하려고 합니다.

 

저희 팀에서 사용하는 Network Library는 Retrofit 을 사용하고 있습니다. 만약 API 응답이 Type 구성에 따라 다양하게 변할 경우, GsonConvertFactory를 사용하여 API 응답을 DTO로 개별적으로 변환하는 방식은 코드의 가독성 문제뿐만 아니라 인간 에러(human error) 발생 가능성도 증가합니다.

 

이러한 문제를 해결하기 위해 Type 구성에 따른 동적으로 DTO을 선택할 수 있도록 Convert Factory를 구성하면, 코드를 사용하는 개발자는 API 응답 정의를 설정하는 데 집중할 수 있으며 원하는 결과를 쉽게 얻을 수 있습니다. 저희 팀은 Data 계층(Layer)의 DataSource에서 Dynamic Response 가 필요하면 MoshiConvertFactory를 사용하였고, 그 외에 모든 통신에 대해서는 GsonConvertFactory 을 사용할 수 있도록 구성하였습니다.

 

Response에 따른 ConvertFactory 변경

 

Dynamic Response 에 따른 MoshiConverterFactory 구성은 Retorfit 호출부에서 Moshi Factory 정의할 수 있도록 구성하였고, 전달받은 Factory로 API 통신할 수 있도록 구성하였습니다.

 

class MoshiApiClient<FACTORY, CLAZZ>(private val factory: PolymorphicJsonAdapterFactory<FACTORY>, private val url: String, private val clazz: Class<CLAZZ>) {
    @Inject
    fun create(client: OkHttpClient): CLAZZ {
        val moshi = Moshi.Builder()
            .add(factory)
            .add(KotlinJsonAdapterFactory())
            .build()
         return Retrofit.Builder()
            .baseUrl(url)
            .addConverterFactory(MoshiConverterFactory.create(moshi))
            .client(client)
            .build()
            .create(clazz)
    }
}

 

Default 통신은 GsonconvertFactory를 사용하였고, Factory구성은 Retrofit.Builder에서 처리하였습니다.

 

class ApiClient<T>(private val networkService: BaseNetworkService, private val clazz: Class<T>) {
    @Inject fun create(client: OkHttpClient): T {
        return Retrofit.Builder()
            .baseUrl(networkService.hostUrl())
            .addConverterFactory(GsonConverterFactory.create())
            .client(client)
            .build()
            .create(clazz)
    }
}

 

 

클린 아키텍처 도입으로 인한 개발자의 피로도 

클린 아키텍처는 유연한 소프트웨어를 구성하는데 도움이 되는 아키텍처는 분명합니다. 하지만 클린 아키텍처의 각 계층(Layer)과 구성요소를 모두 구현하는 것은 상황에 따라서 불필요한 작업을 개발해야 하는 개발자의 피로도를 증가시킬 수 있습니다.

 

예를 들어 신규 API 를 추가해야 한다고 가정한다면 UseCase, Repository, Datasource 를 구성해야 하며 각 구성요소는 interface와 implement 한 class 생성해야 합니다. API 개발하는데 6개의 Class를 생성해야 하며, 더 나아가 Model을 Entity로 변경할 Mapper 도 구성하게 됩니다. 그럼 API 하나 추가하는데 최소 9개 이상의 Class를 구성하게 됩니다.

 

 

Layer Create Template 개발

저희 팀은 개발자의 피로도를 줄이기 위해서 가장 먼저 한 것은 계층(Layer) 생성의 자동화입니다. 앞서 예시처럼 API 호출을 하기 위해서는 개발자가 모든 계층(Layer)의 구성요소를 생성해야 했습니다.

 

저희 팀은 자동으로 계층(Layer)을 생성한다면 개발자의 피로도가 줄여들 것으로 판단하였고, 클린 아키텍처의 Damain 계층(Layer)과 Data 계층(Layer)을 자동으로 생성해 주는 Template 을 개발하였습니다.

 

Template Plugin

 

클린 아키텍처 적용에 대한 피로도를 줄이기 위해서 제공한 Template은 Android Studio Plugin 형태로 제공하였습니다. Android Studio에서 Class 생성하는 방법 대로 Domain, Data 계층(Layer)의 구성요소를 생성할 수 있게 하는 방법을 고민했을 때 Plugin으로 제공하는 것이 개발자가 개발 시 쉽게 사용할 수 있을 것으로 판단하여 Plugin으로 제공하였습니다.

 

Template 생성 화면

 

Template 생성 방법은 생성하고자 하는 모듈 예를 들어 Domain 모듈(Module)이라고 한다면 Domain 모듈(Module)에서 New > Other 에 보면 Clean Architecture Domain 이라는 Template 을 클릭 후 생성하면 됩니다.

 

Domain Layer 생성 시 Prefix Name 설정 화면

 

Template 생성 시 Prefix Class Name을 받게 되어 있습니다. 해당 Prefix Name을 통해 {Prefix Name} 구성요소로 생성이 됩니다. 예를 들면 Domain 계층(Layer)에서 BestItem 이라는 Prefix Name 으로 한다면 BestItemUseCaseModule, BestItemEntity, BestItemRepository, BestItemUseCase 와 같이 Domain Layer(계층)에 대한 구성요소가 생성됩니다.

 

Template 을 통한 Domain Layer 구성요소 생성된 모습

 

 

불필요한 구성요소 제거

저희 팀은 클린 아키텍처를 처음 도입하다 보니 최대한 아키텍처의 구성요소를 적용하는 것을 정책으로 정했습니다. 다만, 저희 회사는 Open API를 사용하지 않고 내부 API 개발자가 프론트(Front) 환경에 맞춰 API의 Response Model을 내려주고 있기에 Mapper 를 통한 Model 을 Entity로 변경하는 작업은 불필요했습니다.

 

Mapper의 기능 최소화

 

UseCase에 따라서 Mapper 의 기능을 생략하고 진행하며 불필요한 구성요소 생성에 대한 개발자의 피로도를 줄였습니다.

 

 

결론

클린 아키텍처는 소프트웨어 시스템을 효율적이고 유지보수 가능한 형태로 설계하기 위한 중요한 개념입니다.

 

저희 팀은 클린 아키텍처의 도입을 저희 프로젝트에 맞게 구성한 부분도 있기에 완벽한 클린 아키텍처라고 볼 수 없지만 최대한 클린아키텍처의 개념을 적용하려고 노력하였으며 그 결과 성공적으로 오픈을 할 수 있었습니다.

 

올바른 계층 구조와 원칙을 준수한다면 소프트웨어 개발의 품질과 생산성을 향상시킬 수 있습니다.


<참고 자료>

 

<원문> 
왜 Android 신규 프로젝트는 클린 아키텍처를 도입하였는가

 

요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.

좋아요

댓글

공유

공유

댓글 0
CJ온스타일 (CJ ENM 커머스)
35
명 알림 받는 중

작가 홈

CJ온스타일 (CJ ENM 커머스)
35
명 알림 받는 중
CJ온스타일을 만드는 IT 담당자들의 고민과 경험을 담습니다.

좋아요

댓글

스크랩

공유

공유

요즘IT가 PICK한 뉴스레터를 매주 목요일에 만나보세요

요즘IT가 PICK한 뉴스레터를
매주 목요일에 만나보세요

뉴스레터를 구독하려면 동의가 필요합니다.
https://auth.wishket.com/login