# 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을 드러내야 한다. 좋은 방향: - `UserEmail` - `Money` - `AuthProviderCode` - `DisplayName` - `TokenTtl` 지양: - `StringWrapper` - `ValueHolder` - `CommonValue` ### 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는 좋은 선택지일 수 있으나 자동 정답은 아님