개발/안드로이드

클린 아키텍처의 기본 개념 (2)

스몰스테핑 2024. 8. 8. 17:31

클린 아키텍처의 개요

1. 경계(boundary) 만들기

  • 저수준 모듈: 상세한 기능 구현, 변경이 잦을만한 요소들의 집합 (예: 문자열 암호화 이후, 로컬 및 원격 DB에 저장) 
  • 고수준 모듈: 핵심적인 비즈니스 로직, 추상적으로 서술 (예: 문자열 저장)

 

2. 클린 아키텍처란?

 

소프트웨어 구조를 설계할 때 지켜야 할 원칙과 방법을 정의한 개념

 

2-1. 의존성 규칙

 

의존성은 반드시 바깥에서 안쪽으로.

 

소스코드의 의존성 방향이 동심원 안쪽으로 향하고 있다.

내부의 원을 Entities라고 가정하면, 내부의 원은 외부에 대해서 몰라야 한다.

외부의 원은 내부의 것을 의존하기 때문에 내부를 알 수 있다.

 

예를 들어, UI의 변경(외부의 원)으로 인해서 이 Entities(내부의 원)을 변경하면 안된다.

 

2-2. 엔티티

 

핵심적인 내용을 캡슐화

 

가장 핵심적인 규칙 행동 로직들을 캡슐화한다.

가장 고수준 모듈이기 때문에 가장 변경되지 않을만한 것들을 넣어야 한다.

어떤 메서드들을 갖는 객체, 데이터 모델, 함수의 집합일 수도 있다. 어찌 되었든 가장 변하지 않을만한 것들이어야 한다.

 

2-3. 유즈케이스

 

애플리케이션이 갖는 기능

 

기능, 비즈니스 로직들을 단일 책임 원칙을 준수하고 캡슐화하면서 구현하는 계층이다.

위에서 말했듯 화살표 의존성 규칙에 따라 유즈케이스의 변경사항이 엔티티에 영향을 주면 안되고, 유즈케이스 외부의 원인 UI, Devices, DB, Framework 등이 유즈케이스에 영향일 줘도 안된다.

 

예를 들어, 애플리케이션 내에서 앱이 사용자 목록을 가져오는 기능이 있다면, 이를 캡슐화한 getUserList 클래스를 만들 수도 있을 것이다. 그러나 이런 유즈케이스 클래스에는 상세한 구현 내용이 없고 추상적으로 만들어진다.

 

이렇게 단일 기능을 갖는 추상적인 인터페이스나 클래스들이 유즈케이스에 포함된다.

 

2-4. 인터페이스 어댑터

 

계층에서 가장 편리한 방식으로 변환

 

일반적으로 유즈케이스에 선언된 추상화된 인터페이스나 abstract 클래스들에 대한 상세 구현을 이 인터페이스 어댑터에서 수행한다.

 

기본적으로 클린 아키텍처는 각자의 계층에서 사용하기 가장 편리한 타입(모델, 데이터모델)을 따로 선언해서 사용한다.

 

유즈 케이스의 어떤 계층에 선언된 내용을 인터페이스 어댑터에서 상속받아서 구현하게 되더라도 결국 유즈케이스를 호출하는 파라미터 타입, 반환 데이터 타입이 일반적으로 엔티티 계층에 선언된 데이터 모델 타입이기 때문에 어떤식으로 데이터를 가지고 오더라도 이 타입에 맞춰서 넘겨주어야 한다.

 

즉 인터페이스 어댑터의 역할은 동심원 계층을 넘나들 때, 그 계층이 가장 이해하기 쉬운 타입으로 변환하는 것이다.

예를 들어, DB의 데이터를 UI에 표현하게 된다면, 계층을 계속 넘나들게 된다. 이때의 형변환을 담당하는 것이 인터페이스 어댑터이다.

 

2-5. 프레임워크 및 드라이버

 

안드로이드 프레임워크

 

프레임워크 및 라이브러리를 포함한 모든 세부적인 내용이 위치하는 곳이다.

안드로이드 SDK, 액티비티 프레임워크, 액티비티 프레그먼트 등등...

 

2-6. 동심원은 꼭 4개?

클린 아키텍처 다이어그램에서 보여지는 이런 동심원은 예시이다.

주어진 상황에 맞춰 더 많은 원을 그리거나 더 적은 원을 그려도 된다.

하지만 변하지 말아야하는 것은 의존성 규칙이다.

또한, 안쪽으로 이동할수록 추상화에 대한 수준이 높아진다는 점바깥쪽으로 이동할수록 세부적인 구현사항이 많아진다는 점. 그렇기에 안쪽에 있는 원들이 더 높은 수준의 범용성을 갖는다.

 

클린 아키텍처는 안드로이드 애플리케이션이라는 프로젝트보다 더 큰 규모와 더 많은 부서와의 협업을 위해 고안된 아키텍처이다. 그래서 안드로이드 개발자들은 엔티티같은 고수준 모듈이 재사용성이 떨어진다고 느낄 수 있다.

 

그러나 만약에 kotlin multi platform 같은 것들을 활용하여 하나의 소스코드로 IOS 앱, Android 앱, Web 앱을 만든다고 생각하면 이런 유즈 케이스나, 엔티티같은 핵심 비즈니스 로직을 공유할 수 있다.

 

2-7. 계층을 횡단하기

  • 인터페이스 분리 원칙
  • 의존성 역전 원칙

 

계층을 횡단하는 우측 하단의 예시를 참고해보자.

제어 흐름(Flow of control)을 동심원에서 살펴보기 쉽게 재구성하여 빨간색 화살표로 흐름을 표시한다.

 

안드로이드의 경우, 화면을 갱신하는 버튼이 있다고 예를 들고, 버튼을 누르면 DB에서 데이트를 불러와서 리사이클러 뷰나 Lazy Column에 UI 구성한다고 가정하자.

 

UI에서 버튼을 누르면, DB까지 가서 데이터를 가져오고 다시 UI까지 해당 데이터가 가져와지는 것.

유즈케이스에서는 클릭을 인식하면 getSomething()을 통해 DB에서 데이터를 가져온다.

데이터는 DTO 타입은 유즈케이스에서 알 수 없는 타입이기 때문에 초록색 원인 인터페이스 어댑터에서 데이터 타입을 변환시키는 wrapper를 통해 유즈케이스가 이해할 수 있는 타입으로 변환시킨다.

유즈케이스가 UI로 넘겨줄 때, UI가 모델을 따로 사용하고 있다면 인터페이스 어댑터의 Presenter에서 변환하여 보내준다.

 

계층을 넘나들 때, 바깥쪽에서 안쪽은 알지만 안쪽에서 바깥쪽은 알지 못하기에 커뮤니케이션을 위해 인터페이스 분리 원칙을 통한 의존성 역전을 통해 통신을 해야한다.

 

내부에서 interface를 선언해주고, 외부에서 해당 interface를 구현하는 것으로 의존성을 역전시켜 내가 원하는 데이터나 이벤트를 전달할 수 있다.

 

3. 요약

  • 클린 아키텍처에서 의존성 방향은 반드시 바깥원에서 안쪽원으로 향해야 한다.
  • 동심원 안쪽으로 갈수록 추상화 레벨이 높아진다(=고수준).
  • 계층을 횡단할 때는 의존성 역전 원칙을 기억하자.

 

안드로이드에서 클린 아키텍처 적용하기

구글이 권장하는 앱 아키텍처

 

앞서 클린 아키텍처를 계속해서 다뤘지만 왜 구글은 앱 아키텍처를 선택했을까?

그 이유로는 범용성 측면에서 서비스나 프로젝트 부서별로 요구사항이 다 다를 수 있기 때문에. 그에 다 알맞는 완벽한 아키텍처는 존재하지 않는 점이 문제였을 것이다. 그렇기에 단 한번의 가이드 문서로써 범용적인 가이드를 제공해야하는 구글 입장에서는 개발자들의 이해와 진행중인 프로젝트 적용에도 쉬운 앱 아키텍처라는 직관적인 아키텍처를 선택했을 것이다.

 

1. 앱 아키텍처 vs 클린 아키텍처

  • 두 아키텍처 모두 계층적인 구조를 갖고 있다. (=관심사 분리)
  • 앱 아키텍처에서는 UI가 Data를 의존하고, 클린 아키텍처에서는 의존관계를 갖지 않는다.
  • 안드로이드 권장 앱 아키텍처가 직관적이며 이해하기 쉽다.
  • 클린 아키텍처는 규모가 큰 앱(시스템)에 더 적합하다.

아무리 안드로이드 애플리케이션이 소규모 프로젝트라고 해도 시간의 흐름에 따라 점점 거대해질 수 밖에 없고, 그렇기에 계층이 확실히 구분된 클린 아키텍처가 확장성이나 유닛테스트의 용이성, 빌드시간 단축 및 여러가지 측면에서 좀 더 우위를 갖기 때문에 클린 아키텍처를 배우는 것이 좋다.

 

모든 서비스, 모든 프로젝트에 알맞는 최고의 아키텍처는 없기 때문에 각자의 상황에 맞춰서 더 적합한 아키텍처를 선택하는 것이지 무조건 클린 아키텍처를 선택하라는 것은 아니다.

 

2. 전형적인 시나리오

 

경계를 나눠보면 다음과 같을 것이다.

 

UI: View, ViewModel

UseCases: UseCase, Repository(interface)

Interface Adapter: Repository(구현체)

Data: LocalDataSource, RemoteDataSource

 

View에서 ViewModel이 UseCase에서 데이터를 요청하고,  UseCase는 Entity를 모델 삼아 Repository를 통해 데이터를 불러온다. 위에서도 말했듯 동심원 안쪽은 바깥쪽을 모르기 때문에 UseCase가 DTO를 알 수 있도록 Repository 구현체와 인터페이스를 만들어 반환 타입을 통해 넘겨준다.

 

3. 클린 아키텍처를 위한 모듈화

 

안드로이드 프로젝트에서 경계를 구분하고 영역을 분리하는 대표적인 방법이 바로 모듈로 분리하는 것이다.

  • :presentation - UI관련 로직 (안드로이드 뷰, 컴포저블, 뷰 모델)
  • :domain - UseCase, Entitiy 로직-클래스 (Model, Repository)
  • :data - Server, Database 로직 (Room, Retrofit, Preferance)

 

 

모듈간 의존성은 다음과 같다.

데이터 모듈과 프레젠테이션이 도메인에 의존하는 형태로 데이터와 프레젠테이션 두 모듈간의 관계는 없다.

그렇기에 독립적으로 테스트하는 것이 가능하다.

 

예를 들어, 프레젠테이션을 테스트하고 싶을때, 프레젠테이션이 의존하는 도메인까지만 의존해서 새로운 앱에서 빌드하는 것도 가능하고 유닛테스트하는 것도 가능하다. 이는 데이터쪽에서도 마찬가지이다.

 

그래서 구글 권장 앱 아키텍처의 경우, 프레젠테이션(UI)를 테스트하고 싶을때, 도메인, 데이터 모든 모듈을 포함해서 빌드해야하지만, 클린 아키텍처는 필요한 모듈만 선택적으로 빌드할 수 있는 장점이 있기에 컴파일 할 코드량이 줄어 빌드시간이 줄어든다는 장점도 존재한다.

 

4. 요약

  • 권장 앱 아키텍처와 클린 아키텍처의 가장 큰 차이점은 의존성 규칙(방향)이다.
  • 권장 앱 아키텍처는 범용성을 갖고, 클린 아키텍처는 대규모 시스템에 적합하다.
  • 클린 아키텍처를 도입하기 위해 안드로이드 모듈화를 진행할 수 있다. 예) :presentation, :domain, :data

 

5. 모듈 만들기 예시

 

프로젝트 하이어리키에서 app을 우클릭하여 Module 새로 만들기를 클릭한다.


 

:domain 모듈을 만들어 볼 것이기 때문에, 새롭게 뜨는 창에서 Java or Kotlin Library를 선택한다.

 

왜냐하면 도메인 모듈이란 것은 안드로이드 의존성이 없는 범용적인 모듈, 가장 핵심적인 로직만 담겨야하기 때문에 안드로이드 라이브러리보단, Java or Kotlin Library로 만드는 것이 좋다.


 

이후 기다리면 build.gradle.kts에 :domain이 생기고, domain으로 따로 폴더도 생기는 것을 확인할 수 있다.

domain 폴더 내부의 MyClass는 아무것도 없는 빈 class로 초기 생성시 생기기 때문에 삭제해도 무방하다.


 

여기서 package를 나눈다.

model, repository, usecase


 

이후 :presentation 모듈과 :data 모듈을 만든다.

이 두 모듈은 안드로이드 라이브러리로 만든다.

 


plugins {
    id("java-library")
    alias(libs.plugins.jetbrainsKotlinJvm)
}

java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

 

도메인 gradle에 들어가보면 위와 같이 자바 버전이 나온다.

 

compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }

 

저 자바 버전을 app gradle의 버전과 동일하게 맞춘다.

나머지 생성한 모듈들도 마찬가지로 맞춰준다.

 


dependencies {

    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.appcompat)
    implementation(libs.material)
    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.androidx.espresso.core)

    implementation(project(":domain"))
}

 

위에서 말했듯이 :Presentation과 :Data는 :Domain에 의존해야한다.

그 의존성을 각각의 dependencies에 추가해준다.


 

이제 app에서 해당 data나 presentation을 의존해서 작업할 수 있다.

기본적인 세팅자체는 다 끝났으나 추가적으로 app gradle의 dependencies에 다음처럼 추가해주자.

dependencies {
    implementation(project(":domain"))
    implementation(project(":data"))
    implementation(project(":presentation"))
    
    ...
}

 

app은 사실 모든 내용을 알아야하기 때문이다.

 

domain은 아무것도 의존하지 않는 코어한 모듈로.

data와 presentation은 domain을 의존한다.

app은 세 가지 모듈을 다 의존한다.

 

이렇게 클린 아키텍처를 사용하기 위한 기본적인 설정이 완료된다.