안드로이드 아키텍처 학습 노트 목차

Clean Architecture — domain·data·presentation·의존성 규칙

왜 "의존성의 방향"이 그렇게 중요한가

[01]에서 본 UI·도메인·데이터 3계층을 더 엄격하게 밀어붙인 것이 Clean Architecture다. 그 전부가 의존성 규칙(dependency rule) 한 문장으로 요약된다 — 모든 의존성은 안쪽(도메인)을 향한다.

왜 이 방향이 중요할까? 소프트웨어에서 가장 자주 바뀌는 것프레임워크·라이브러리·DB·UI다 — 안드로이드 버전이 오르고, Retrofit이 Ktor로 바뀌고, Room이 다른 DB로, View가 Compose로 바뀐다. 반대로 가장 안 바뀌는 것비즈니스 규칙이다("주문 금액은 음수일 수 없다", "회원 등급에 따라 할인율이 다르다"). 그런데 보통의 코드에선 비즈니스 규칙이 프레임워크에 얽혀 있어서 — Retrofit을 바꾸면 비즈니스 로직까지 다시 짜야 한다. Clean Architecture는 이를 뒤집는다. 안 바뀌는 비즈니스 규칙(도메인)을 중심에 두고, 자주 바뀌는 것(프레임워크·DB·UI)이 도메인에 의존하게 만들어 — 바깥이 바뀌어도 안쪽은 안전하게 한다. (출처: Android — Domain layer / R. Martin Clean Architecture.)

의존성 규칙 — 화살표가 안쪽을 향한다

다이어그램 로딩 중…

여기서 반직관적인 부분이 하나 있다 — 데이터 계층(RepositoryImpl)이 실제로 DB·네트워크를 다루는데도, 화살표가 데이터에서 도메인으로(안쪽으로) 향한다. 어떻게? 비밀은 **의존성 역전 원칙(DIP)**이다. 도메인이 Repository 인터페이스(약속)를 정의하고, 데이터 계층이 그 인터페이스를 구현한다. 그래서 도메인은 "누가 구현하는지" 모른 채 인터페이스에만 의존하고, 데이터는 도메인의 인터페이스를 보고 따른다 — 화살표가 안쪽을 향하는 것이다.

이 한 가지 트릭이 주는 실질적 이득 — 데이터 구현을 통째로 갈아끼워도 도메인은 한 줄도 안 바뀐다. REST를 GraphQL로, Room을 다른 DB로 바꿔도, 같은 UserRepository 인터페이스를 구현하기만 하면 도메인·UI는 그대로다. 테스트에선 *가짜 구현(FakeUserRepository)*을 끼워 도메인을 프레임워크 없이 검증한다.

세 계층의 책임

  • domain(도메인) — 중심, 순수 코틀린. 안드로이드 의존성이 0이다(import android.*이 하나도 없다). 엔티티(도메인 모델)·UseCase·Repository 인터페이스를 담는다. 프레임워크가 없으니 가장 안정적이고, JVM 단위 테스트가 빠르고 쉽다. 비즈니스 규칙의 성역이다.
  • data(데이터) — 바깥. Repository 구현·DTO(서버 응답 모양)·Mapper·데이터 소스(Retrofit/Room). 도메인이 정의한 인터페이스를 구현하고, 서버/DB의 모양(DTO)을 도메인 모델로 변환한다.
  • presentation(프레젠테이션) — 바깥. ViewModel·UI 상태·Compose. UseCase를 호출해 받은 도메인 모델을 화면에 맞는 UI 모델로 빚는다([02 MVVM]).

모델이 계층마다 다른 이유 — Mapper

처음 보면 "왜 같은 User를 세 번이나 만드나" 싶지만, 이유가 분명하다. *서버 JSON의 모양(full_name, snake_case, nullable 범벅)*과 비즈니스가 다루고 싶은 깔끔한 모양과 *화면에 보여줄 포맷(날짜를 "3분 전"으로)*은 서로 다른 관심사다. 이를 한 모델로 합치면 — 서버가 필드 이름을 바꾸는 순간 화면 코드까지 깨진다.

// domain — 순수·안정적 (서버/화면이 어떻든 비즈니스가 보는 모양)
data class User(val id: Long, val name: String)
interface UserRepository { suspend fun getUser(id: Long): User }

// data — DTO(서버 모양) + Mapper + 인터페이스 구현
data class UserDto(val id: Long, val full_name: String?)   // 서버 JSON 그대로
fun UserDto.toDomain() = User(id, full_name ?: "이름 없음")  // Mapper: 변환·정규화
class UserRepositoryImpl(private val api: Api) : UserRepository {
    override suspend fun getUser(id: Long) = api.fetch(id).toDomain()
}

이 분리 덕에 — 서버가 full_namedisplayName으로 바꿔도 DTO와 Mapper 한 곳만 고치면 끝이고, 도메인·UI는 그대로다. 계층 경계의 Mapper변경의 충격을 흡수하는 완충재 역할을 한다.

트레이드오프 — 격리 vs 보일러플레이트

Clean Architecture는 공짜가 아니다모델 3벌·매퍼·인터페이스·UseCase코드가 늘고 초기 구축이 무겁다. 작은 앱에 이걸 다 갖추면 *과한 의식(ceremony)*이 된다. 그래서 규모에 맞춰 조절한다.

  • 크고 오래갈 앱·여러 팀·복잡한 비즈니스 규칙 → 격리의 이득이 보일러플레이트 비용을 압도한다.
  • 작은 앱·프로토타입도메인 계층(UseCase)을 생략하고 ViewModel이 Repository를 직접 호출하는 게 실용적이다([01]의 "도메인은 선택"). 그래도 Repository 인터페이스 + DTO/도메인 분리는 작은 앱에서도 대개 이득이다.

"규칙을 위한 규칙"이 되지 않게 — 핵심은 의존성을 안쪽으로 향하게 하는 것이고, 모델 3벌·UseCase 같은 세부는 앱 규모에 맞춰 가감한다.

정리 — 비즈니스를 프레임워크에서 떼어낸다

Clean Architecture의 핵심 — 의존성은 안쪽(도메인)을 향하고(자주 바뀌는 바깥이 안 바뀌는 안쪽에 의존), 도메인은 순수 코틀린(프레임워크 0)이며 인터페이스를 정의, 데이터가 그 인터페이스를 구현(DIP) — 그래서 구현을 갈아끼워도 도메인은 안전, 계층 경계에서 Mapper로 모델을 변환해 변경의 충격을 흡수한다. 단, 보일러플레이트 비용이 있으니 앱 규모에 맞게 계층 깊이를 조절한다.

한 줄 요약 — 의존성 규칙=화살표가 안쪽(도메인)을 향함(자주 바뀌는 프레임워크/DB/UI가 안 바뀌는 비즈니스에 의존). 도메인이 Repository 인터페이스 정의→데이터가 구현(DIP)→구현 교체해도 도메인 안전·Fake로 테스트. 계층: domain(순수 코틀린·엔티티·UseCase) / data(구현·DTO·Mapper) / presentation(ViewModel·UI). 모델은 계층마다(DTO/도메인/UI)+Mapper가 변경 흡수. 비용(보일러플레이트) vs 격리 — 규모로 조절.

(출처: Android — Domain layer / Architecture recommendations / R. Martin Clean Architecture / Now in Android.)

MVVM·단방향 데이터 흐름(UDF)Repository·UseCase — 단일 출처·비즈니스 캡슐화