1. 개요
자바/스프링(Java/Spring) 생태계에서 웹 애플리케이션을 개발하다 보면 “데이터 상태 변경 비즈니스 로직을 서비스 계층(Service)에 작성할 것인가, 엔티티(Entity) 내부에 부여할 것인가” 하는 고전적이면서도 매우 중요한 갈림길에 마주하곤 합니다.
도합 2.0 버전의 ‘회원 코칭 관계(User Relationship)’ 핵심 모듈을 설계하면서, 단순히 Setter로 값을 밀어 넣는 형태의 **‘빈약한 도메인 모델(Anemic Domain Model)‘**이 가져오는 한계와 이를 극복하기 위해 엔티티 스스로 행동하도록 유도하는 ‘풍부한 도메인 모델(Rich Domain Model)’ 구조를 도입하여 데이터 무결성을 확보해 낸 아키텍처적 당위성을 기술적인 차원에서 명쾌하게 짚어봅니다.
2. 핵심 내용
2-1. 빈약한 도메인 모델(Anemic) vs 풍부한 도메인 모델(Rich) 비교분석
- 기존 방식의 문제점 (Setter 남용):
레거시 코드에서 흔히 발견되는 ‘빈약한 도메인 모델’ 방식은 엔티티 클래스가 단지 데이터만 담아두는 수동적인 DTO 통으로 전락하고, 모든 제어와 복잡한 계산이 서비스 계층에 포진하게 됩니다.
예를 들어 “코칭 요청 수락 시 상태를 ACCEPTED로 바꾸고 매칭 시각을 기록해야 한다”는 정책이 있을 때, 서비스가 엔티티의 모든 필드에 대해
.setStatus(ACCEPTED),.setMatchedAt(LocalDateTime.now())를 손수 처리해 주는 절차지향적인 구조를 이룹니다. 이 경우 서비스가 엔티티의 속살을 낱낱이 알아야 하며 결합도가 급상승합니다. - 풍부한 도메인 모델 도입 (Entity 메서드 주도):
엔티티가 데이터뿐만 아니라 그 데이터를 조작할 수 있는 **스스로의 책임과 행위(Behavior)**를 한 몸에 품도록 설계했습니다. 서비스 계층에서는 단지
relation.accept()라는 단 한 줄의 명령만 전달하고, 구체적인 데이터 세팅과 정합성 가공은 엔티티가 자율적으로 처리합니다.
// UserRelationshipEntity.java
public void accept() {
// 내부에서 직접 본인의 상태값과 매칭 시각을 안전하게 제어합니다.
this.status = UserRelationshipStatusEnum.ACCEPTED;
this.matchedAt = LocalDateTime.now();
}2-2. Entity 내부 비즈니스 로직 구현이 주는 4가지 확실한 강점
이렇게 Setter의 대문을 걸어 잠그고 객체에 스스로의 책임을 부여했을 때 얻어지는 명확한 기술적 혜택은 다음과 같이 요약할 수 있습니다.
- 데이터 무결성(Data Integrity) 보장: Setter가 전방위로 열려 있으면 누구나 아무 시점에나 값을 훼손할 수 있습니다. 수락 메서드를 직접 호출하게 함으로써 “상태 변경과 시간 기록”이라는 두 행위가 언제나 단 한 벌의 트랜잭션 단위처럼 **원자적(Atomic)**으로 일어나게 만들어 유령 데이터 누출을 원천 예방합니다.
- 응집도(Cohesion) 극대화: “데이터를 가진 객체가 그 데이터를 처리하는 룰도 가지고 있어야 한다”는 객체지향의 절대 명제를 성실히 따릅니다. 향후 비즈니스 수락 조건이나 상태 제약 조건이 바뀌더라도 여러 서비스 코드를 순회할 필요 없이, 해당 엔티티 클래스 단 한 곳만 보수하면 끝납니다.
- 의도를 드러내는 명료한 가독성: 코드 속에서 단순 값 대입을 나타내는
relation.setStatus(ACCEPTED)대신 비즈니스 행위 그 자체를 표현하는relation.accept()를 마주함으로써 코드를 읽는 개발자의 인지 과부하가 말끔히 해소됩니다. - 도메인 규칙의 전역적 재사용성: 이 수락 비즈니스 룰이 배치를 돌거나, CMS 관리자 페이지에 이식되거나, 일반 사용자 API로 들어오더라도 오직
relation.accept()코드 한 줄로 완벽히 재활용되어 중복 코드가 전면 차단됩니다.
2-3. 은행 금고 비유를 통한 직관적 정리 및 추천 라이브러리 검토
이러한 구조적 성찰을 실생활 비유로 표현하자면 **“금고 문이 활짝 열려있어 은행 직원(Service)이 금고(Entity) 속으로 직접 기어들어가 5만원을 빼오고 수기로 장부를 적는 방식(Anemic)“**과, **“금고 문은 단단히 닫혀있고 바깥에 단지 안전하게 노출된 ‘5만원 출금’ 버튼(accept)만 눌러주면 금고 내부 기계가 자동으로 장부를 남기고 돈을 건네는 안전한 방식(Rich)“**의 차이와 정확히 맞닿아 있습니다.
- 추천 오픈소스 라이브러리:
MapStruct: 엔티티 내부를 비즈니스 메서드로 꽁꽁 싸매고 Setter를 닫았을 때, 클라이언트에서 넘어오는 안전한 DTO 데이터를 엔티티로 변환하거나 그 반대로 안전하게 프로젝션해주는 컴파일 타임 기반 고성능 매핑 도구입니다. 런타임 오버헤드가 전혀 없으며, Setter 없이도 안전한 객체 생성을 조력해 주어 적극 권장합니다.Lombok: 자바 컴파일 시점에 자동으로@Getter,@Builder,@NoArgsConstructor(access = AccessLevel.PROTECTED)등의 보일러플레이트 바이트코드를 생산해주는 표준 라이브러리입니다. 특히 Setter는 과감히 제외하고 빌더(Builder)와 Getter만을 깔끔하게 사용해 닫힌 엔티티 아키텍처를 구성할 때 찰떡궁합을 보여줍니다.