5.3 KiB
mapper separation 기준
목적
매퍼는 한 모델의 데이터를 다른 모델로 번역하는 역할만 맡는다.
비즈니스 규칙, 영속성 접근, 외부 호출, 부수효과를 매퍼에 섞지 않는다.
공식 의미
- DTO는 프로세스/경계 사이에서 데이터를 옮기는 객체다.
- DTO와 domain object 사이를 옮기는 assembler/mapper를 둘 수 있다.
- Spring DataBinder는 입력 바인딩이 강력하지만, 바인딩 대상 설계를 신중히 해야 하며 보안상 주의가 필요하다.
- MapStruct 같은 매퍼 도구는 type-safe bean mapping 코드를 생성한다.
- 기존 target 객체 갱신은
@MappingTarget같은 명시적 update mapping으로 표현할 수 있다.
기본 규칙
1. 매퍼는 “번역”만 한다
매퍼의 책임은 아래 중 하나다.
- request DTO -> command
- domain -> response DTO
- persistence entity -> domain
- domain -> persistence entity
- external DTO -> internal model
- internal model -> external DTO
즉 “형태를 바꾸는 일”까지만 한다.
2. 비즈니스 규칙은 매퍼에 넣지 않는다
다음은 매퍼 책임이 아니다.
- 상태 전이 결정
- 권한 판정
- 에러 코드 결정
- 정책 선택
- 유효성 최종 판정
- 도메인 invariant 강제의 주 책임
단, 단순한 정규화/포맷 수준의 보조 변환은 허용될 수 있다.
3. 매퍼에서 repository / external API / service 호출 금지
매퍼는 pure mapping에 가깝게 유지한다.
금지:
- DB 조회
- 외부 API 호출
- 다른 aggregate 탐색을 위한 repository 호출
- 메시지 발행
- 파일/네트워크 접근
매핑에 필요한 부가 데이터가 있으면 호출자가 먼저 준비해서 매퍼에 전달한다.
4. 경계마다 매퍼를 분리한다
다음 경계를 하나의 매퍼로 섞지 않는다.
- web request/response 변환
- persistence entity 변환
- external integration 변환
예:
UserWebMapperUserPersistenceMapperVaultApiMapper
처럼 경계별로 분리한다.
5. DTO / Domain / Entity를 직접 섞지 않는다
하나의 매퍼가 다음을 동시에 다루며 의미를 섞지 않게 한다.
- request DTO
- domain
- JPA entity
- external API payload
필요하면 경계별로 mapper를 여러 개 둔다.
6. 매퍼는 기술 세부보다 구조적 대응 관계를 표현
좋은 매퍼는 아래를 명확하게 보여야 한다.
- 어떤 source를 어떤 target으로 바꾸는가
- 어떤 필드가 대응되는가
- 어떤 값이 누락되거나 기본값 처리되는가
반대로 아래는 매퍼에 새기지 않는다.
- HTTP 상태 코드
- DB 트랜잭션
- serialization 포맷 정책
- retry/fallback 정책
7. update mapping은 명시적으로만
기존 target을 수정하는 매핑은 “새로 생성하는 매핑”과 구분한다.
기본:
- create mapping
- update mapping
을 별도 메서드로 둔다.
기존 객체 갱신은 side effect가 있으므로 이름과 계약을 분명히 한다.
8. null / empty / default 처리 정책을 숨기지 않는다
매퍼는 다음을 명확히 해야 한다.
- null source를 허용하는가
- null field를 무시하는가
- null이면 target을 덮어쓰는가
- empty collection을 그대로 넣는가
- 기본값을 넣는가
정책이 중요하면 호출자/standard 문서에서 먼저 정하고 매퍼에 일관되게 반영한다.
9. mapper는 가능한 한 결정적이고 테스트 가능해야 한다
같은 입력이면 같은 결과가 나와야 한다.
지양:
- 현재 시각 생성
- 랜덤 값 생성
- 환경값 조회
- thread-local/MDC 접근
정말 필요하면 호출자가 값을 주입한다.
10. request binding과 도메인 생성은 구분한다
Spring DataBinder/Web binding이 request를 객체로 바꿔 주더라도, 그 객체를 domain으로 승격하는 과정은 별도 매퍼/assembler 또는 factory에서 통제한다.
즉:
- web binding = 입력 수집
- mapper = 구조 변환
- domain factory/value object = 의미/불변식 부여
11. persistence mapper는 DB nullable/오염 상태를 domain으로 직접 흘리지 않는다
entity -> domain 매핑에서는:
- nullable column
- legacy 값
- 잘못된 저장 데이터 를 명시적으로 처리한다.
필요하면 예외를 던지거나 복원 규칙을 적용하지만, 조용히 의미를 바꾸지 않는다.
12. external integration mapper는 wire format을 코어에 새기지 않는다
외부 API JSON/XML/HTTP payload 구조는 integration 전용 mapper에서 소화한다. core/application/domain은 외부 wire format 세부를 몰라야 한다.
13. 자동 매핑 도구를 써도 책임은 그대로
MapStruct 같은 도구를 써도 아래 원칙은 변하지 않는다.
- 비즈니스 로직을 매퍼에 넣지 않는다
- update/create를 구분한다
- 경계별 매퍼를 분리한다
- null/default 정책을 숨기지 않는다
도구는 구현 보조일 뿐 설계 기준을 대체하지 않는다.
프로젝트 기준 요약
- 매퍼는 번역만 한다
- 비즈니스 정책/DB 조회/외부 호출 금지
- web / persistence / integration 매퍼 분리
- create mapping과 update mapping 분리
- null/default 정책 명시
- 결정적이고 테스트 가능하게 유지
- domain 의미 부여와 매핑을 혼동하지 않음