Anemic Domain Model
정의
Anemic Domain Model(빈혈 도메인 모델)은 도메인 객체에 getter와 setter만 있고 행위는 거의 없으며, 모든 도메인 로직이 Service 객체에 있는 설계다. Martin Fowler가 2003년에 안티패턴으로 지목했고, Eric Evans도 같은 진단에 동의했다.
Fowler는 이것을 “꽤 오래 존재해 온 안티패턴"이라고 부르면서, 제대로 된 Domain Model의 지지자로서 이것이 유행하는 것은 좋은 일이 아니라고 못 박는다.
증상
Fowler의 서술이 정확하다.
빈혈 도메인 모델의 기본 증상은 처음 보면 진짜처럼 보인다는 것이다. 도메인 공간의 명사를 따라 이름 붙은 객체들이 있고, 이 객체들은 진짜 도메인 모델이 가진 풍부한 관계와 구조로 연결되어 있다.
함정은 행위를 들여다볼 때 드러난다. 이 객체들에는 행위가 거의 없고, getter와 setter의 자루에 가깝다는 것을 알게 된다. 실제로 이런 모델에는 “도메인 객체에 도메인 로직을 넣지 말라"는 설계 규칙이 딸려오는 경우도 많다. 대신 모든 도메인 로직을 담아 계산을 수행하고 그 결과로 모델 객체를 갱신하는 Service 객체 집합이 존재한다. 이 Service들은 도메인 모델 위에 살면서 도메인 모델을 데이터로 사용한다.
코드로 보면 다음 차이다.
// 빈혈 - 세 필드가 항상 함께 바뀌어야 한다는 규칙이 코드 어디에도 없다
showSeat.setStatus("HELD")
showSeat.setHeldUntil(now.plusMinutes(7))
showSeat.setHeldByReservationId(reservationId)
// 규칙이 엔티티 안에
showSeat.hold(reservationId, now.plusMinutes(7))
왜 문제인가
객체지향 설계의 기본에 반한다
이 안티패턴의 근본적 공포는 그것이 데이터와 프로세스를 결합한다는 객체지향 설계의 기본 아이디어에 완전히 반한다는 점이다. 빈혈 도메인 모델은 사실 절차적 스타일 설계일 뿐이다.
더 나쁜 것은 많은 사람들이 빈혈 객체를 진짜 객체라고 생각하여, 객체지향 설계가 무엇에 관한 것인지를 완전히 놓친다는 점이다.
도메인 모델의 비용은 다 치르고 이득은 하나도 얻지 못한다
Fowler는 객체지향 순수주의보다 이 논거를 더 근본적인 것으로 제시한다.
빈혈 도메인 모델의 문제는 도메인 모델의 모든 비용을 치르면서 그 이득은 하나도 산출하지 못한다는 것이다. 주된 비용은 DB 매핑의 어색함이고, 그것은 보통 O/R 매핑 계층 전체로 귀결된다. 이 비용은 복잡한 로직을 조직하기 위해 강력한 객체지향 기법을 사용할 때만 값어치가 있다.
모든 행위를 Service로 빼내면 본질적으로 Transaction Script가 되고, 그러면 도메인 모델이 가져올 수 있는 이점을 잃게 된다.
즉 빈혈 도메인 모델은 두 접근법 중 어느 쪽의 이득도 얻지 못하는 상태다. Transaction Script를 쓸 거라면 O/R 매핑 계층의 비용을 치르지 않는 편이 낫고, O/R 매핑 계층을 둘 거라면 로직을 도메인 객체에 넣어야 한다.
여기서 중요한 단서가 있다. Fowler는 P of EAA에서 Domain Model이 항상 최선의 도구는 아니라고 명시했다. 로직이 단순하면 Transaction Script가 정당한 선택이다. 문제는 도메인 모델을 표방하면서 실제로는 Transaction Script인 상태다.
Service Layer와의 혼동
이 지점이 실무에서 가장 헷갈린다. 많은 객체지향 전문가가 도메인 모델 위에 절차적 Service 계층을 두는 것을 권한다. Fowler는 이것이 빈혈을 정당화하는 논거가 아니라고 선을 긋는다.
이것은 도메인 모델을 행위 없는 것으로 만들라는 논거가 아니다. 실제로 Service Layer 지지자들은 행위가 풍부한 도메인 모델과 함께 Service Layer를 사용한다.
Evans의 계층 정의가 경계를 명확히 한다.
Application Layer(Fowler가 말하는 Service Layer): 소프트웨어가 해야 할 일을 정의하고 표현력 있는 도메인 객체들이 문제를 풀도록 지시한다. 이 계층은 얇게 유지된다. 비즈니스 규칙이나 지식을 담지 않고, 작업을 조율하여 아래 계층의 도메인 객체 협력에 위임할 뿐이다. 비즈니스 상황을 반영하는 상태를 갖지 않지만, 사용자나 프로그램의 작업 진행 상태는 가질 수 있다.
Domain Layer(또는 Model Layer): 비즈니스의 개념, 비즈니스 상황에 대한 정보, 비즈니스 규칙을 표현할 책임을 진다. 비즈니스 상황을 반영하는 상태가 여기서 통제되고 사용된다. 저장의 기술적 세부사항은 인프라에 위임되더라도 그렇다. 이 계층이 비즈니스 소프트웨어의 심장이다.
핵심은 Service Layer가 얇다는 것이다. 핵심 로직은 모두 도메인 계층에 있다. Evans는 Service 패턴 항목에서 같은 점을 다시 강조한다.
더 흔한 실수는 행위를 적절한 객체에 넣는 것을 너무 쉽게 포기하고 점차 절차적 프로그래밍으로 미끄러지는 것이다.
계층 분리 자체는 옳다는 점도 분명히 한다. 도메인 객체에 행위를 넣는 것이 영속성과 표현 책임을 도메인 로직에서 분리하는 계층화와 모순되지 않는다. 도메인 객체에 들어가야 하는 것은 검증, 계산, 비즈니스 규칙 같은 도메인 로직이다.
자가 진단
- 엔티티에
setXxx가 있다. 대체로 빈혈의 신호다. 특히 여러 필드가 항상 함께 바뀌어야 하는데 각각의 setter로 노출되어 있으면 그 규칙은 코드에 존재하지 않는다 - 같은 검증 로직이 두 곳 이상에 있다. 컨트롤러 검증에도 있고 Service에도 있고 다른 Service에도 있다
- Service 메서드가 엔티티의 필드를 읽어
if로 판단한 뒤 다른 필드를 쓴다 - 엔티티 클래스에 필드와 getter/setter 외에 아무것도 없다
- “이 규칙은 어디 있나"라는 질문에 파일 이름 하나로 답할 수 없다
Microsoft 가이드는 이 진단을 다음 문장으로 요약한다.
비즈니스 로직이 엔티티 밖 서비스 클래스에 살면 빈혈 도메인 모델을 만들게 된다. 그 안티패턴은 비즈니스 규칙을 도메인 모델 안에 표현한다는 DDD의 이점을 무너뜨린다.
왜 이렇게 흔한가
Fowler는 원인을 확신하지 못한다면서도 두 가지를 짚는다.
- 제대로 된 도메인 모델로 작업해 본 적이 없는 사람이 많다. 특히 데이터 중심 배경에서 온 경우
- 일부 기술이 이것을 장려한다. Fowler가 예로 든 것은 J2EE의 Entity Bean이며, 그가 POJO 도메인 모델을 선호하는 이유 중 하나다
현대적 맥락으로 옮기면 다음이 같은 역할을 한다.
- 엔티티를 DTO처럼 다루게 만드는 프레임워크 관행 (모든 필드에 setter, 기본 생성자 노출)
domain/application/infrastructure폴더만 만들고 로직 배치는 그대로 두는 것. 폴더 구조는 규칙을 강제하지 못한다- ORM 매핑 편의를 위해 모든 필드를 가변으로 열어두는 것
벗어나는 방법
- setter를 없앤다. 대신 의미 있는 상태 전이 메서드를 만든다.
setStatus대신hold(),confirm(),expire() - 함께 바뀌어야 하는 필드를 하나의 메서드로 묶는다. 세 필드를 각각 세팅하는 코드가 세 곳에 있다면 그것이 규칙이 없다는 증거다
- Service에 있는
if판단을 엔티티로 옮긴다. 판단에 필요한 데이터가 그 엔티티 안에 있다면 판단도 그 안에 있어야 한다 - 교체 가능한 값은 Value Object로 만든다. 상태마다 필요한 데이터가 다르면 sealed 계층으로 표현 불가능한 상태를 컴파일 단계에서 막는다
- 여러 Aggregate에 걸치는 규칙만 Domain Service로 남긴다. 하나의 Aggregate 안에서 판단할 수 있는 것은 그 Aggregate의 메서드다
관련 개념
- Entity와 Value Object - 행위를 어디에 둘지 판단하는 기준
- Repository와 Service - Domain Service와 Application Service의 경계
- Aggregate - 불변식을 강제할 책임이 있는 단위
- DDD 개요 - DDD를 표방하는 프로젝트가 실제로는 여기 머무는 경우
참고 자료
- Martin Fowler, Anemic Domain Model
- Martin Fowler, Patterns of Enterprise Application Architecture (2002), Domain Model / Transaction Script / Service Layer
- Eric Evans, Domain-Driven Design (2003), Layered Architecture 및 Services 패턴
- Microsoft, Tactical DDD