Files

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을 드러내야 한다.

좋은 방향:

  • 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는 좋은 선택지일 수 있으나 자동 정답은 아님