137 lines
3.0 KiB
Markdown
137 lines
3.0 KiB
Markdown
# value object 예시
|
|
|
|
## 좋은 예시 1: 이메일 Value Object
|
|
|
|
```java
|
|
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
|
|
|
|
```java
|
|
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
|
|
|
|
```java
|
|
@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처럼 사용
|
|
|
|
```java
|
|
public record User(Long id, String name) {}
|
|
```
|
|
|
|
**문제:**
|
|
|
|
- User는 identity가 본질인 entity일 가능성이 높다
|
|
- 값 객체로 만들면 의미가 흐려진다
|
|
|
|
## 나쁜 예시 2: mutable Value Object
|
|
|
|
```java
|
|
public class UserName {
|
|
private String value;
|
|
|
|
public void setValue(String value) {
|
|
this.value = value;
|
|
}
|
|
}
|
|
```
|
|
|
|
**문제:**
|
|
|
|
- 생성 후 불변이 아니다
|
|
- 검증/정규화 이후 상태가 깨질 수 있다
|
|
|
|
## 나쁜 예시 3: 의미 없는 래퍼
|
|
|
|
```java
|
|
public record NameString(String value) {}
|
|
```
|
|
|
|
**문제:**
|
|
|
|
- business meaning이 약하다
|
|
- 검증/정규화/행위가 전혀 없다
|
|
- 래퍼 비용만 생길 수 있다
|
|
|
|
## 나쁜 예시 4: Value Object에서 외부 의존
|
|
|
|
```java
|
|
public class UserEmail {
|
|
public boolean exists(UserRepository repository) {
|
|
return repository.existsByEmail(value);
|
|
}
|
|
}
|
|
```
|
|
|
|
**문제:**
|
|
|
|
- 값 객체가 외부 의존과 오케스트레이션을 떠안는다
|
|
- 순수한 값 의미 타입이 아니다
|