본문으로 건너뛰기

결정-0051: 도메인 모델과 영속 모델을 분리한다

상태

Accepted

맥락

도메인 엔티티와 영속 객체를 하나로 둘지 나눌지는 백엔드 구조의 갈림길입니다. 나누면 도메인이 프레임워크를 모르게 되지만 매핑 계층이 생기고, 붙이면 매핑이 없어지는 대신 도메인이 프레임워크 어노테이션을 답니다.

ORM 을 쓰는 구성에서는 이 선택이 실제 트레이드오프입니다. 엔티티가 곧 영속 객체이므로 나누려면 클래스를 두 벌 정의하고 그 사이에 변환기를 둡니다. 필드 하나를 추가할 때 도메인 · 영속 객체 · 변환기 세 곳을 고쳐야 하고, 그 비용이 매일 발생합니다. 그래서 나누지 않는 선택이 합리적일 수 있으며, 실제로 그 선택을 한 구성을 운영한 경험이 있습니다.

그 구성이 성립하려면 두 조건이 유지되어야 했습니다. 엔티티가 스스로 불변식을 검증할 것, 그리고 프레임워크 없이 단위 테스트가 가능할 것입니다. 두 조건 중 하나라도 깨지면 나누지 않은 이점이 소멸합니다. 두 조건은 도구가 검사하지 않으므로 사람이 계속 지켜야 합니다.

결정

두 모델을 분리합니다. 도메인 엔티티는 순수 Java 이며 프레임워크와 영속성 라이브러리를 알지 못합니다.

항목 결정 내용
도메인 엔티티 어노테이션을 달지 않습니다. 정적 팩토리와 비공개 생성자로 생성을 통제합니다
변환 위치 저장소 구현 안의 비공개 메서드가 담당합니다. 별도 매퍼 계층을 만들지 않습니다
생성 경로 업무 낱말의 생성 메서드와 저장소 전용 재구성 메서드를 나눕니다
강제 수단 ArchUnit이 domain 패키지의 바깥 의존을 차단합니다

jOOQ 에서는 분리가 비용이 아닙니다. 생성 레코드와 도메인 엔티티가 애초에 별개이므로 나누는 것이 기본 상태이고, 오히려 붙이려는 쪽이 작업입니다. ORM 구성에서 이 결정을 막던 세 곳 수정 비용이 발생하지 않습니다.

매퍼 계층을 따로 두지 않는 것이 두 번째 핵심입니다. 저장소는 어차피 레코드를 다루므로 변환의 자연스러운 자리이며, 매퍼 클래스를 만들면 파일이 늘고 그 자체가 유지 대상이 됩니다.

검토한 대안

도메인이 생성 레코드를 그대로 쓴다

구분 내용
장점 변환 코드가 아예 없습니다
단점 생성 레코드는 스키마의 모양이지 도메인의 모양이 아닙니다. 컬럼이 추가되면 도메인 타입이 함께 바뀌고, 불변식을 걸 자리도 없습니다
기각 사유 도메인이 스키마에 종속됩니다. 분리하지 않는 선택의 이점(매핑 비용 절감)은 이미 없는 상태이므로 얻는 것이 없습니다

매퍼 계층을 별도로 둔다

구분 내용
장점 저장소가 변환 책임을 갖지 않아 역할이 더 좁아집니다
단점 애그리거트마다 클래스가 하나씩 늘고, 저장소와 매퍼가 항상 함께 변경됩니다
기각 사유 항상 같은 이유로 함께 바뀌는 둘을 나눈 것이므로 분리의 근거가 없습니다

결과

도메인 단위 테스트가 프레임워크 컨텍스트 없이 실행됩니다. 검증 속도가 빨라지고, 무엇보다 도메인 테스트를 쓰는 것 자체가 분리 조건의 상시 검사가 됩니다. 컨텍스트를 요구하기 시작하면 그 시점에 조건이 깨진 것이 드러납니다.

감수하는 것이 둘입니다.

변경 감지가 없습니다. 애그리거트를 바꾼 뒤 저장을 호출하지 않으면 오류 없이 아무 일도 일어나지 않습니다. ORM 에서 넘어온 사람이 가장 자주 겪는 사고이며, 명령 서비스의 단위 테스트가 저장 호출을 확인하는 것이 방어입니다.

애그리거트 저장을 손으로 씁니다. 자식 컬렉션의 연쇄 저장과 고아 제거가 자동으로 일어나지 않으므로 루트 저장소가 그 순서를 직접 다룹니다.

© 2026 dev.goraebap