이번 글은 아래의 영상으로 부터 얻은 내용을 바탕으로 작성하였습니다.

빈혈 도메인, 쓸모없는 유스케이스, 비대한 뷰모델에 대하여를 주제로 DroidKnights 2023에서 강연하신 박종혁님(카카오스타일)의 영상입니다.

빈혈(anemic) 도메인 모델과 쓸모없는 유스케이스 그리고 비대한(Bloated) 뷰모델에 대해 생각해보기 Video: 빈혈(anemic) 도메인 모델과 쓸모없는 유스케이스 그리고 비대한(Bloated) 뷰모델에 대해 생각해보기

개발에서 발생할 수 있는 문제점들로 빈혈 도메인 모델, 쓸모없는 유스케이스, 비대한 뷰모델 세 가지가 소개되었습니다.

- 빈혈, 무기력한, 빈약한(Anemic) 도메인 모델

도메인 모델(클래스)이 데이터만 가지고 있고, 어떠한 로직이 없는 상태, AnemicDomainModel

  • 안티 패턴 중 하나(특정 경우에는 적절할 수 있으므로 무조건 사용하지 말아야 하는 것은 아닙니다)
  • 객체의 의미와 책임을 제대로 반영하지 못하기 때문에 객체지향적인 설계를 벗어납니다.
  • 단순 구조체와 다를 바 없으므로, 모델을 생성하는 의미가 없다고 볼수 있습니다.
data class Developer(
    val name: String,
    val age: Int,
)

data class DeveloperList(
    val list: List<Developer>
)

- 쓸모없는 유스케이스

유스케이스(클래스)가 단순히 레포지토리와 뷰모델을 연결만 해주는 상태, 특별한 기능이 없음

  • 비즈니스 로직을 서버에 위임하는 아키텍처에서 주로 보입니다.
  • 안티 패턴으로 간주되기도 하지만, 코드 일관성 및 미래의 수정에 대비하는 등의 이유로 사용되기도 합니다.
interface SpeakerRepository {
    suspend fun getSpeakerList(): List<Speaker>
}

class LoadSpeakerListUseCase(
    private val speakerRepository: SpeakerRepository
) {
    suspend operator fun invoke() = speakerRepository.getSpeakerList()
}

오직 speakerRepository.getSpeakerList()를 위해서 유스케이스가 쓰입니다.

- 비대한(Bloated) 뷰모델

뷰모델이 과하게 크고 복잡한 상태

  • 뷰모델이 너무 많은 책임과 로직을 가지고 있으면 유지보수성이 떨어집니다.
class SpeakerListViewModel(
    private val loadSpeakerList: LoadSpeakerListUseCase
) : ViewModel() {
    private val speakerList // ...

    fun load() {
        viewModelScope.launch {
            speakerList = loadSpeaker()
            // ...
        }
    }

    fun getYoungDevelopers() = speakerList.filter { 
        it.age < 30 
    }

    // ...
}

이러한 문제들을 해결하는 방법

명확한 정답은 없음, 끊임없는 고민으로 적절한 방법을 찾도록 합니다. 팀 프로젝트라면 팀원들과의 끊임없는 토의로 가이드라인을 찾아가는 것이 중요합니다.

카카오스타일 지그재그 앱 개발팀이 적용한 방법

팀원들과의 토의를 통해 다음과 같은 가이드라인으로 합의점을 찾았다고 합니다.

  • 가이드라인
    • 프레젠테이션 레이어에 도메인 로직을 구현하지 않는다.
    • 단일 도메인 모델에 대한 비즈니스 로직은, 도메인 모델이 책임지도록 한다.
    • 여러 도메인 모델에 대한 비즈니스 로직은, 유스케이스가 해결할 수 있다.

1. 프레젠테이션 레이어에 도메인 로직을 구현하지 않는다.

  • 뷰모델은 프레젠테이션 레이어(UI 레이어)이므로, 뷰모델에는 데이터를 뷰에 표현하거나, 사용자 상호작용을 위한 로직을 구현하도록 한다.

2. 단일 도메인 모델에 대한 비즈니스 로직은, 도메인 모델이 책임지도록 한다.

  • 도메인 모델은 필요한 로직을 직접 구현해야 한다. 빈혈 도메인 모델을 개선시키기 위해 관련 로직을 구현하는 것이다.
  • 뷰모델에서 도메인 모델을 직접 다루는 로직은 구현해서는 안된다.
  • 프레젠테이션 로직이 아니라면, 도메인 메서드에 구현하는 것을 우선으로 고려한다.

3. 여러 도메인 모델에 대한 비즈니스 로직은, 유스케이스가 해결할 수 있다.

  • 도메인 로직은 아래 기준에 따라 유스케이스로 구현되어야 한다.
    • 여러 도메인 모델이 참조되는 복잡한 비즈니스 로직
    • 프레젠테이션 레이어에서 직접 데이터 레이어를 참조하지 않도록 하는 Wrapper 레이어 로직
      • 간단하게 SAM interface로 구현 가능

개선 예시

위와 같은 가이드라인에 따라 아래와 같이 개선이 가능합니다.

도메인 모델에 책임을 부여

data class Developer(
    val name: String,
    val age: Int,
){
    fun isYoung() = age < 30   
}

data class DeveloperList(
    val list: List<Developer>
){
    fun getYoungDevelopers() = list.filter { 
        it.isYoung() 
    }
}

여러 도메인 모델에 대한 비즈니스 로직을 유스케이스에서 구현

class LoadSpeakerListWithCompanyNameUseCase(
    private val speakerRepository: SpeakerRepository
) {
    suspend operator fun invoke(company: Company) = speakerRepository.getSpeakerList().concatCompanyName(company)
}
class SpeakerListViewModel(
    private val loadSpeakerList: LoadSpeakerListUseCase
) : ViewModel() {
    private val speakerList // ...

    fun load() {
        viewModelScope.launch {
            speakerList = loadSpeaker()
            // ...
        }
    }

    // 도메인 모델 내에서 필터링 로직이 구현된 메서드를 호출하여 사용
    fun getYoungDevelopers() = speakerList.getYoungDevelopers()
    // ...
}

정리

  • 빈혈 도메인 모델을 개선하기 위해서 도메인 모델에 책임을 부여할수 있다.
  • 여러 도메인 모델에 대한 비즈니스 로직을 유스케이스에서 구현하여, 유스케이스의 책임을 확장할 수 있다.
  • 뷰모델은 프레젠테이션 레이어이므로, 도메인 모델을 직접 다루는 로직은 구현해서는 안된다.

이러한 개선 방법이 무조건 정답이라고 볼수 없으며, 현재 프로젝트의 구조, 상황에 따라 적절한 가이드라인을 찾아가는 것이 중요합니다.