[Android] Clean Architecture 개념

문승연·2023년 12월 27일
0

Clean Architecture

목록 보기
1/1

1. 클린 아키텍처(Clean Architecture)는 무엇일까?

요즘 개발을 하다 보면 프로젝트에 클린 아키텍처를 적용했다던가 클린 아키텍처 경험이 있는 개발자를 구한다던가. 클린 아키텍처(Clean Architecture)에 대한 이야기가 굉장히 많이 나온다.

그렇다면 클린 아키텍처란 무엇일까? 클린 아키텍처란 아주 유명한 책인 『클린 코드(Clean Code)』를 저술한 로버트 마틴(Robert C. Martin)이 제시한 개념으로 복잡한 소프트웨어 시스템을 보다 관리 가능하고 유지보수 가능한 형태로 구축하기 위한 지침을 제공하기 위해서 등장했다.

주요 원칙

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

의존성 역전 원칙(Dependency Inversion Principle)
고수준 모듈은 저수준 모듈에 의존해서는 안되며, 양쪽 모듈 모두 추상화에 의존해야 합니다. 이를 통해 느슨한 결합을 유지할 수 있습니다.

경계(Boundary)의 분리
시스템을 여러 영역으로 나누고, 각 영역 사이의 인터페이스를 정의하여 각 영역의 독립성을 보장합니다.

인터페이스 분리 원칙(Interface Segregation Principle)
클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않아야 합니다. 즉, 인터페이스는 클라이언트의 요구에 딱 맞는 형태로 분리되어야 합니다.

클린 아키텍처하면 가장 많이 나오는 청사진 이미지이다. 프레임워크와 드라이버, 인터페이스 어댑터, 애플리케이션 업무 규칙, 엔터프라이즈 업무 규칙 총 4가지 계층으로 나뉘어져 있으며 각 계층은 바깥에서 안쪽으로 의존성을 가진다.

2. 클린 아키텍처 왜 사용할까?

A라는 앱을 개발하는데 B라는 앱과 통합을 하게되었다. 이때 받을 수 있는 요구사항들을 가정해보자.

A 앱 시스템이 잘 되어있으니 A 앱의 핵심 기능은 유지하고 UI와 DB 쪽만 바꿔주세요.

아니면 혹은

A 앱이 너무 잘되니 서비스를 웹으로 확장해봅시다.

만약 이런 상황에서 클린 아키텍처를 도입하지 않았다면? 상당히 머리가 아플 것이다. 코드를 하나 하나 뜯어보면서 이건 냅두고 이건 고치고 근데 이거 건들면 다른게 또...

하지만 클린 아키텍처를 도입했다면 좀 더 수월하게 위 작업을 수행할 수 있을 것이다.

첫번째 요구사항의 경우 애플리케이션 업무 규칙, 엔터프라이즈 업무 규칙은 냅두고 프레임워크와 드라이버, 인터페이스 어댑터만 수정하면 된다.

두번째 요구사항의 경우도 프레임워크와 드라이버 영역만 수정하면 된다.

이처럼 클린 아키텍처는 필수적인 비즈니스 로직은 바꾸지않고 언제든지 DB와 프레임워크에 구애받지않고 교체할 수 있게 해주는 셈이다.

좀 더 구체적으로 정의하면 아래처럼 4가지 이유로 정의할 수 있다.

유지 보수성 향상
클린 아키텍처는 의존성 관리 및 모듈화를 통해 애플리케이션의 유지 보수성이 단순 MVVM 패턴보다 향상된다. 각 계층은 명확하게 정의되고 분리되어 있으므로 변경 요구사항에 맞춰 변경이 쉽게 이루어질 수 있다.

테스트 용이성
유지 보수성 향상과 동일 이유로 각 계층이 독립적으로 테스트 가능하도록 설계되어 있어 테스트 용이성을 높일 수 있다. 유닛 테스트, 통합 테스트 등을 보다 쉽게 테스트를 진행할 수 있다.

미래 확장성
새로운 기능을 추가하거나 기존 기능을 변경하려는 경우 변경의 범위를 최소화하며 애플리케이션의 확장에 유용하다.

비즈니스 로직 분리
비즈니스 로직을 다른 계층으로 분리하여 비즈니스 룰과 데이터베이스 접근을 분리시킨다. 이것은 비즈니스 로직을 단일 위치에서 유지하고 이해하기 쉽게 만든다.

3. 클린 아키텍처 in 안드로이드


일반적인 클린 아키텍처 구조에서는 의존성이 무조건 바깥에서 안쪽으로 향하지만 안드로이드에서는 계층을 프레젠테이션 계층(UI 계층), 도메인 계층, 데이터 계층 총 3가지로 나누고 프레젠테이션 계층과 데이터 계층이 각각 도메인 계층을 바라보며 의존성을 가지는 형태로 구성된다.

MVVM 패턴과 더불어 데이터 흐름과 함께 좀 더 알아보기 쉬운 이미지는 아래와 같다.

각 계층이 의미하는 바는 다음과 같다.

  • 프리젠테이션 계층(Presentation Layer)
    - 뷰(View): 직접적으로 플랫폼 의존적인 구현, 즉 UI 화면 표시와 사용자 입력을 담당. 단순하게 프레젠터가 명령하는 일만 수행한다.
    - 뷰모델(ViewModel): 사용자 입력이 왔을 때 어떤 반응을 해야 하는지에 대한 판단을 하는 영역. 무엇을 그려야 할지도 알고 있는 영역이다.
  • 도메인 계층(Domain Layer)
    - 유즈 케이스(Use Case): 비즈니스 로직이 들어 있는 영역.
    - 엔티티(Entity): 앱의 실질적인 데이터.
  • 데이터 계층(Data Layer)
    - 리포지터리(Repository): 유즈 케이스가 필요로 하는 데이터의 저장 및 수정 등의 기능을 제공하는 영역으로, 데이터 소스를 인터페이스로 참조하여, 로컬 DB와 네트워크 통신을 자유롭게 할 수 있도록 함.
    - 데이터 소스(Data Source): 실제 데이터의 입출력이 여기서 실행되는 부분.

여기서 Repository는 왜 도메인 계층과 데이터 계층에 걸쳐져 있는지, 이렇게 하면 위에서 언급한 클린 아키텍처 원칙에 위배되는 게 아닌지 궁금해할수 있다.

이는 도메인 계층에서 UseCase로 데이터를 요청할 때, Repository를 참조하게되는데 이렇게되면 상위 계층인 도메인 계층이 데이터 계층에 의존성을 가지게 되므로 클린 아키텍처 원칙에 위배되는 게 맞다.

이건 상위 모듈에서 인터페이스를 생성하고 의존 관계를 역전시켜 모듈을 분리함으로써 해결할 수 있다. 이를 의존성 역전이라고 한다. 사실 위에 클린 아키텍처 주요 원칙에도 설명되어있다.

위 이미지처럼 도메인 계층에 리포지터리 인터페이스를 만들고 하위 계층인 데이터 계층에서 리포지터리 구현체가 인터페이스를 참조하게하면 된다.

4. 예제 코드

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

UserData를가져오는 UseCase를 구현한다고 가정해보자.

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

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

이때 정의한 UserDataRepository는 interface로 구성되어있다.

interface UserDataRepository {
	suspend fun getUserData(): Result<UserDataEntity>
}

UserDataUseCase의 리포지토리는 DI로 정의하여 @Provides를 구성해야한다. API 통신은 Activity의 Lifecycle을 따르도록 DI Component를 ActivityRetainedComponene 로 정의한다.

@Module
@InstallIn(ActivityRetainedComponent::class)
class UserDataUseCaseModule{
    @Provides
    fun provideUserDataUseCase(repository: UserDataRepository) = UserDataUseCase(repository)
}

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

class UserDataRepositoryImpl @Inject constructor(
    private val remote: UserDataRemoteDataSource
) : UserDataRepository {
    override suspend fun getUserData(): Result<UserDataEntity> {
        val response = remote.getUserData()
        // todo.. response에 대한 처리 로직 추가
    }
}

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

@Module
@InstallIn(ActivityRetainedComponent::class)
class UserDataRepositoryModule {
    @Provides
    fun provideUserDataRepository(
        remote: UserDataRemoteDataSource
    ): UserDataRepository = UserDataRepositoryImpl(remote)
}

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

레퍼런스)
1. [Android] 요즘 핫한 Clean Architecture 왜 쓰는 거야?
2. 왜 Android 신규 프로젝트는 클린 아키텍처를 도입하였는가

profile
"비몽(Bemong)"이라는 앱을 개발 및 운영 중인 안드로이드 개발자입니다.

0개의 댓글