
최근 프로젝트들을 보면 대부분 클린 아키텍처를 적용한 것을 볼 수 있다. 클린 아키텍처를 사용하지 않은 프로젝트를 찾기 힘들 정도로 가장 인기 있는 아키텍처임이 분명하다. 나 또한 지금까지 만들어온 프로젝트에 모두 클린 아키텍처를 적용하였고, 이 과정에서 한 가지 의문이 들었다.
바로 Useless UseCase와 Anemic Domain Model. 이에 대한 자세한 내용은 2023년 드로이드 나이츠에서 박종혁 님께서 연사 하신 내용이 있으니 관심 있다면 꼭 한번 보길 추천한다. 나는 이 영상에서 소개한 내용에 대해 너무나 깊이 공감하여 4번은 돌려본 것 같다. 그래서 이를 해결하기 위한 방법들을 끊임없이 고민해 왔지만 1년이 지나도록 그 답을 찾지 못했다.
하지만 얼마 전 함께 우테코를 수료한 크루 포르가 이에 대한 답을 제시해 주었다. 이에 기반하여 내가 그동안 고민해 온 클린 아키텍처의 한계점과 이를 해결할 한 가지 의견을 제시해보고자 한다.
클린 아키텍처란?
클린 아키텍처는 Robert C. Martin(Uncle Bob)이 제안한 아키텍처 패턴으로 최근 안드로이드 개발 페러다임에서 사실상 표준처럼 자리 잡았다.

- Presentation → Domain ← Data 형태의 의존성 방향을 가진다.
- Domain 계층에 Repository Interface를 만들고 Data 계층에 Repository의 구현체를 만든다.
- 이를 통해 저수준에 의존하지 않고 고수준에 의존하는 형태, 즉 DIP(Dependency Inversion Principle) 원칙을 지킬 수 있다.
- Domain 계층이 Data 계층을 몰라도 서로 다른 모듈 간에 데이터를 주고받을 수 있다.
간단하게 추상화한 일반적인 클린 아키텍처의 코드 구조는 다음과 같다.
data
├── UserResponse.kt
├── repository/UserRepositoryImpl.kt
└── datasource/UserRemoteDataSource.kt
domain
├── model/User.kt
├── repository/UserRepository.kt
└── usecase/GetUserUseCase.kt
presentation
├── UserViewModel.kt
└── UserUiState.kt
클린 아키텍처의 문제점
1. Useless UseCase

먼저 Usecase란 무엇일까 ? 비즈니스 로직을 재사용할 수 있도록 하나의 단위로 캡슐화한 객체를 의미한다. 예시로 다음과 같은 비즈니스 로직을 가진 앱을 만든다고 가정하자
“유저가 이메일로 로그인한다.
입력된 이메일이 유효해야 하며, 서버에 로그인 요청을 보낸다.”
class LoginUseCase(
private val repository: UserRepository
) {
suspend operator fun invoke(email: String, password: String): Result<User> {
// (1) 이메일 검증
if (!email.contains("@")) {
return Result.failure(IllegalArgumentException("이메일 형식이 아닙니다."))
}
// (2) Repository를 통해 실제 로그인 로직 실행
return try {
val user = repository.login(email, password)
Result.success(user)
} catch (e: Exception) {
Result.failure(e)
}
}
}
만약 이러한 비즈니스 로직을 하나의 객체로 캡슐화하지 않았다면 이 로직을 사용하는 ViewModel 마다 동일한 코드를 반복해서 작성해야 한다. 또한 로그인 규칙이 변경되면 이 로직을 사용하는 ViewModel 모두를 일일이 수정해야 하므로 유지보수 비용이 커지고 이는 곧 단일 책임 원칙(SRP)을 위반하는 결과를 낳는다.
반대로 이 비즈니스 로직을 하나의 UseCase로 묶어 재사용하도록 설계하면 로직이 변경되더라도 UseCase 한 곳만 수정하면 전체 시스템이 업데이트된다. 즉, UseCase는 비즈니스 로직을 한 곳에 모아 재사용성, 유지보수 용이성, 책임 분리라는 장점을 제공한다.
UseCase를 사용하다 보면 종종 다음과 같은 코드를 만나게 된다. 여기선 해당 비즈니스 로직을 사용하는 곳이 단 한 곳이라고 가정한다.
class GetUserUseCase(
private val repository: UserRepository
) {
suspend operator fun invoke(userId: String) = repository.getUser(userId)
}
이 UseCase의 문제점은 무엇일까?
- Repository를 단순히 호출하기만 하는 중간 다리 역할에 불과하다
- "비즈니스 로직"이라고 할 수 없다
- 클린 아키텍처의 형식을 맞추기 위한 보일러플레이트일 뿐이다
- UseCase의 본래 목적인 "재사용 가능한 비즈니스 로직 캡슐화"와 무관하다
이러한 UseCase를 Useless UseCase라고 한다. Useless UseCase는 형식을 위한 형식일 뿐 실질적인 가치를 제공하지 못한다.
물론 미래의 확장성을 위해서 이런 UseCase를 만들 수도 있다. Repository를 호출만 하는 UseCase라 할지라도 이를 재활용하는 곳이 많다면 추후 비즈니스 로직이 추가되었을 때 UseCase만 수정하면 된다. 하지만 미래를 위한 확장 가능성을 과도하게 열어놓는 행위는 오버엔지니어링이라고 생각한다.
그렇기 때문에 UseCase를 사용해야 한다면 다음 상황에 해당하는지 고민해 봐도 좋을 것 같다.
- 명확한 비즈니스 로직인가 ? (적어도 Repository를 호출만 하는 역할이 아닐 때)
- 동일한 비즈니스 로직이 반복되어 이를 재사용 하도록 만들어야 하는가?
2. Anemic Domain Model

다음은 전형적인 Domain Model의 예시다.
data class User(
val id: String,
val name: String
)
코드의 문제는 무엇일까?
- 데이터만 있고 그 데이터와 관련된 비즈니스 규칙이 없다
- 즉, Domain Model이 DTO나 구조체처럼 작동하는 상태다
- 클린 아키텍처의 특성상 도메인이 데이터 계층의 응답 모델을 알 수 없기 때문에 이런 변환용 모델이 필수적이다
이러한 Domain Model을 빈약한 도메인 모델(Anemic Domain Model)이라고 한다. 필자는 지금까지 프로젝트를 진행하면서 앱이 커질수록 이러한 빈약한 도메인 모델이 늘어나는 것을 경험하였다. 때문에 Domain Model이 각자의 책임과 역할을 가질 수 있도록 많은 고민을 하였다.
하지만 안드로이드는 Server와 Client의 관계 속에서 Server에서 받은 데이터를 정말 순수하게 화면에 렌더링 하는 역할을 하는 경우가 종종 있다. 이 경우 Domain Model에게 특정한 책임을 부여하는 것이 불가능하다고 느꼈고 이를 해결하는 방법을 늘 고민해 왔다.
그렇다면 지금까지 제시한 의견들을 고려해 볼 때 클린 아키텍처는 진짜 Clean이라는 형용사 그대로의 깨끗한 아키텍처 일까?
객체지향 관점에서 다시 생각하기

객체지향이란 무엇인가?
조영호 님의 객체 지향의 사실과 오해를 바탕으로 내가 정의한 객체지향은 다음과 같다.
- 객체지향은 서비스가 제공하는 비즈니스 로직들을 객체라는 단위로 묶어 표현한다.
- 객체는 데이터(상태)와 그 데이터를 다루는 행동(메서드)을 하나로 묶는 자율적인 존재다.
- 자율적인 객체란 "상태"와 "행위"를 함께 지니며 스스로 자기 자신을 책임지는 객체를 의미한다.
현재의 Domain Model은 객체지향적인가?
Anemic Domain Model은 이 원칙을 위반하고 있다. 상태만 있고 행동이 없는 객체는 진정한 의미의 "도메인 모델"이 아니라 그저 "데이터 구조체"일 뿐이다.
결론적으로 클린 아키텍처에서 말하는 Domain Model은 종종 데이터 구조체처럼 사용되며, 객체지향의 핵심 개념인 자율적인 객체를 실천하기 어렵다. 이러한 관점에서 봤을 때 클린 아키텍처에서 말하는 Domain Model은 객체지향적이지 않다.
어떻게 개선할 수 있을까?
핵심 아이디어는 간단하다.
Domain 계층을 역할과 책임을 가진 자율적인 객체들의 집합으로 만들자
개선 원칙
- Domain Model과 UseCase는 비즈니스 로직이 있을 때만 추가한다
- Domain Model이 사라졌으니 UI 계층에서 직접 Data 계층의 데이터를 받아온다
- Presentation → Domain → Data 형태로 의존성 방향을 수정한다
구체적 개선 방안
1. UI 레이어에 Mapper 구현
// Data Layer - DTO 정의
data class UserDto(
val id: String,
val name: String,
)
// UI Layer - Mapper 구현
fun UserDto.toUiModel() = UserUiModel(
id = id,
name = name,
)
// UI Layer - ViewModel
class UserViewModel(
private val userRepository: UserRepository
) : ViewModel() {
fun loadUser(userId: String) {
viewModelScope.launch {
val result = userRepository.getUser(userId)
_uiState.value = result.toUiModel() // UI에서 변환
}
}
}
비즈니스 로직이 없는 단순 변환이라면 굳이 Domain 계층에서 처리할 필요가 없다. UI 계층에서 필요한 형태로 바로 변환하면 된다.
2. Repository를 Data 레이어로 이동
기존 클린 아키텍처에서 Repository의 인터페이스를 Domain 계층에, 구현체를 Data 계층에 두는 방식은 Domain 계층에서 Data 계층으로 향하는 의존성을 역전시키기 위함이었다.
하지만 Repository가 DTO를 반환하는 구조로 변경하면서 Repository에서 Data 레이어에 대한 의존이 필수적이게 되었고, Repository 인터페이스는 더 이상 Domain 레이어에 있을 필요가 없어졌다.
// Data Layer - Repository 인터페이스와 구현체 모두 Data에 위치
interface UserRepository {
suspend fun getUser(userId: String): UserDto // DTO 반환
suspend fun getUsers(): List<UserDto>
}
class UserRepositoryImpl(
private val api: UserApi
) : UserRepository {
override suspend fun getUser(userId: String): UserDto {
return api.getUser(userId)
}
}
Domain 계층은 언제 사용할까?
Domain 계층은 명확한 비즈니스 로직이 있을 때만 사용한다. 예를 들어 다음과 같은 예시가 있다.
- 여러 Repository를 조합해야 하는 복잡한 로직
- 도메인 규칙이 적용되어야 하는 경우
- 상태와 행위를 가진 도메인 객체가 필요한 경우
이런 경우에만 Domain 계층을 추가하고 그렇지 않다면 과감히 생략하는 것이 더 깔끔하다.
마치며
클린 아키텍처는 분명 훌륭한 아키텍처 패턴이다. 하지만 모든 상황에 완벽한 해답은 아니다. 형식을 위한 형식, 구조를 위한 구조가 되어서는 안 된다. 옛날 옛적 얘기를 할 때 단군 할아버지가 나오시는데 개발자에게 단군 할아버지는 엘런 튜링이 아닐까 싶다.
앨런 튜링 할아버지 때부터 지금까지 프로그래밍이란 기술이 발전해 오며 수많은 시행착오를 겪고 동일한 문제들을 겪었을 것이다. 그리고 이런 문제를 해결하는 방법들을 획일화하고 최적화한 것이 디자인 패턴이나 아키텍처가 탄생한 계기이며 이것들 조차 또 다른 문제들을 겪으며 점점 발전해 나가는 것 같다고 생각한다.
이러한 규칙과 원칙들은 특정한 무언가를 더 잘 만들기 위한 일종의 "수단과 방법"이고 그 수단과 방법이 옳다고만은 할 수 없는 것 같다. 정답은 없으니까. 대신 개선하고자 하는 것, 목표하는 것을 정확히 이루기 위해 그 수단과 방법을 더 잘 사용하는 것을 고민해봐야 할 것 같다.
이 글에서 제시한 방법이 정답은 아니다. 하지만 적어도 우리가 습관적으로 따라 하던 패턴에 대해 한 번쯤 의문을 가져보는 계기가 되었으면 한다. 여러분의 프로젝트에서는 어떤 아키텍처가 정말로 필요한가? 한번 고민해 보는 건 어떨까?
마지막으로 이 관점을 제시해준 포르에게 감사의 말을 남기며 이만 글을 마친다.
