Files
project-auth-server/docs/examples/design/value-object.md
T

3.0 KiB

value object 예시

좋은 예시 1: 이메일 Value Object

public record UserEmail(String value) {

    public UserEmail {
        Objects.requireNonNull(value, "value must not be null");
        value = value.trim().toLowerCase(Locale.ROOT);

        if (value.isBlank()) {
            throw new IllegalArgumentException("email must not be blank");
        }
        if (!EMAIL_PATTERN.matcher(value).matches()) {
            throw new IllegalArgumentException("invalid email format");
        }
    }

    public static UserEmail from(String raw) {
        return new UserEmail(raw);
    }
}

왜 좋은가:

  • 문자열 의미를 타입으로 끌어올린다
  • 정규화와 검증이 한 곳에 모인다
  • 값 기반 equality가 자연스럽다

좋은 예시 2: 금액 Value Object

public record Money(BigDecimal amount) {

    public Money {
        Objects.requireNonNull(amount, "amount must not be null");
        amount = amount.setScale(2, RoundingMode.HALF_UP);

        if (amount.signum() < 0) {
            throw new IllegalArgumentException("amount must not be negative");
        }
    }

    public Money add(Money other) {
        return new Money(this.amount.add(other.amount));
    }
}

왜 좋은가:

  • 숫자 primitive를 그대로 흘리지 않는다
  • scale/음수 금지 규칙이 타입에 들어간다
  • 값 관련 행위가 같이 있다

좋은 예시 3: entity와 분리된 domain Value Object

@Entity
@Table(name = "users")
public class UserJpaEntity {
    private String email;
}

public record UserEmail(String value) { ... }

public final class UserPersistenceMapper {
    public User toDomain(UserJpaEntity entity) {
        return User.restore(
                UserEmail.from(entity.getEmail())
        );
    }
}

왜 좋은가:

  • persistence 문자열과 domain 의미 타입이 분리된다
  • domain invariant를 mapper 경계에서 회복한다

나쁜 예시 1: identity를 가진 것을 Value Object처럼 사용

public record User(Long id, String name) {}

문제:

  • User는 identity가 본질인 entity일 가능성이 높다
  • 값 객체로 만들면 의미가 흐려진다

나쁜 예시 2: mutable Value Object

public class UserName {
    private String value;

    public void setValue(String value) {
        this.value = value;
    }
}

문제:

  • 생성 후 불변이 아니다
  • 검증/정규화 이후 상태가 깨질 수 있다

나쁜 예시 3: 의미 없는 래퍼

public record NameString(String value) {}

문제:

  • business meaning이 약하다
  • 검증/정규화/행위가 전혀 없다
  • 래퍼 비용만 생길 수 있다

나쁜 예시 4: Value Object에서 외부 의존

public class UserEmail {
    public boolean exists(UserRepository repository) {
        return repository.existsByEmail(value);
    }
}

문제:

  • 값 객체가 외부 의존과 오케스트레이션을 떠안는다
  • 순수한 값 의미 타입이 아니다