5.9 KiB
value object 기준
목적
Value Object는 식별자보다 값 자체가 본질인 도메인 개념을 표현한다.
문자열, 숫자, primitive 조합으로 흩뿌려진 의미를 작은 타입으로 끌어올려,
- 의미를 드러내고
- 불변식을 한 곳에 모으고
- 잘못된 조합을 줄이는 것이 목적이다.
공식 의미
- Value Object는 개념적 identity가 없다.
- Value Object는 생성 후 immutable하게 다루는 것이 기본이다.
- 값이 같으면 서로 interchangeable 하다.
- 값 기반 객체는 identity-sensitive 연산(
==, identity hash, synchronization)에 의존하지 않는다. equals/hashCode는 identity가 아니라 상태값 기준이어야 한다.
기본 규칙
1. identity가 아니라 값이 본질이면 Value Object를 우선 검토
다음은 Value Object 후보다.
- 이메일
- 사용자 이름
- 금액
- 통화
- 기간
- 주소
- 토큰 문자열
- 공개키 식별자
- provider code
- 정규화된 path/host/url 일부
- 비즈니스 규칙이 붙은 ID wrapper
질문:
- “무엇인가”보다 “어떤 값인가”가 본질인가?
- 같은 값이면 같은 것으로 취급해야 하는가?
- 생성 시점에 검증/정규화 규칙을 묶고 싶은가?
2. Value Object는 기본적으로 immutable
Value Object는 생성 후 상태가 바뀌지 않게 설계한다.
기본:
- final field
- setter 없음
- 변경이 필요하면 새 인스턴스 반환
변경 가능한 컬렉션/객체를 내부에 들고 있으면 defensive copy 또는 immutable snapshot을 사용한다.
3. equality는 값 기준
Value Object의 동등성은 값으로 판단한다.
기본:
equals/hashCode구현- record를 쓸 수 있으면 record 우선 검토
==비교 금지- identity-based lock/synchronization 금지
4. 생성 시점에 불변식을 강제
Value Object는 가능한 한 생성 시점에 유효한 상태만 허용한다.
예:
UserEmail.from(...)에서 trim/lowercase/형식 검증Money.of(...)에서 음수 금지/scale 정리UserName.from(...)에서 길이/문자 규칙 검증
“일단 넣고 나중에 확인”을 금지한다.
5. primitive obsession을 줄이는 방향으로 도입
다음과 같은 경우 Value Object 도입을 우선 검토한다.
- 같은
String이지만 의미가 여러 개라 실수 가능성이 큼 - 여러 곳에서 같은 검증/정규화가 반복됨
- 메서드 시그니처에서 의미가 안 드러남
- 잘못된 값 조합을 타입 수준에서 줄이고 싶음
6. 너무 사소한 래퍼는 만들지 않는다
다음은 도입을 보류할 수 있다.
- 검증/정규화/행위가 전혀 없음
- 의미가 너무 자명하고 혼동 위험이 낮음
- 래퍼 비용이 실제 이득보다 큼
즉 모든 primitive를 기계적으로 감싸지 않는다.
7. Value Object는 도메인 언어를 사용
이름은 기술 표현이 아니라 business meaning을 드러내야 한다.
좋은 방향:
UserEmailMoneyAuthProviderCodeDisplayNameTokenTtl
지양:
StringWrapperValueHolderCommonValue
8. Value Object는 nullable 대신 명시적 의미를 우선
가능하면 Value Object 자체는 non-null로 다룬다.
부재 표현이 필요하면:
- Optional 반환
- nullable boundary 입력
- 별도 상태 타입
- empty/unknown 값을 실제 business state로 둘지 신중히 검토
null을 Value Object 의미의 일부처럼 쓰지 않는다.
9. Value Object는 DTO/Entity와 분리
Value Object는 domain 의미 타입이다.
기본:
- request DTO field를 그대로 Value Object로 바인딩하지 않음
- entity field를 그대로 Value Object로 대체하지 않고 매핑 전략을 명시
- DTO <-> domain, entity <-> domain 변환에서 Value Object를 생성/복원
10. 컬렉션을 포함하는 Value Object는 특히 신중
컬렉션이 들어가는 Value Object는 아래를 만족해야 한다.
- 컬렉션 자체가 immutable/unmodifiable
- 원소도 가능한 한 immutable
- equality/hashCode 의미가 분명함
- 순서 중요 여부가 명확함
11. 행위가 있어도 된다. 단, 값 의미와 관련된 행위여야 한다
Value Object는 단순 data carrier일 필요는 없다.
허용 예:
- 정규화
- 포맷 변환
- 비교
- 계산
- 조합
- 규칙 기반 convenience method
금지 예:
- repository 호출
- 외부 API 호출
- 전역 상태 의존
- 객체 그래프 조립의 중심이 되는 orchestration
12. persistence는 domain 의미를 우선하되 별도 매핑으로 해결
JPA entity는 persistence 제약을 받으므로 Value Object와 1:1로 같아야 할 필요는 없다.
기본:
- entity <-> domain mapper에서 Value Object 생성/복원
- 가능하면 embeddable/owned type 등 적절한 persistence 모델 사용 검토
- persistence 편의 때문에 domain Value Object를 포기하지 않음
13. record는 좋은 기본 선택지일 수 있다
Java record는 값 중심 타입 표현에 잘 맞을 수 있다. 단, 아래를 만족할 때 사용한다.
- 불변 구조가 자연스럽다
- 값 기반 equality가 맞다
- 생성 시 검증/정규화를 canonical constructor/factory로 명확히 표현할 수 있다
단, record를 쓴다고 자동으로 좋은 Value Object가 되는 것은 아니다.
14. Value Object는 작은 타입이지만 경계 비용을 줄여야 한다
도입 후 얻는 이득:
- 의미가 타입에 드러남
- 검증 중복 감소
- 잘못된 조합 감소
- 테스트 용이성 증가
단, 의미 없는 래퍼 남발은 금지한다.
프로젝트 기준 요약
- identity보다 값이 본질이면 Value Object 우선 검토
- 기본은 immutable
- equality는 값 기준
- 생성 시점에 불변식 강제
- primitive obsession 줄이기
- 너무 사소한 래퍼는 지양
- DTO/Entity와 분리
- persistence는 mapper/별도 매핑 전략으로 해결
- record는 좋은 선택지일 수 있으나 자동 정답은 아님