init: 클린 기반 auth 서버 설계
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
# common module 예시
|
||||
|
||||
## 좋은 예시 1: common 대신 owning module에 둠
|
||||
|
||||
```text
|
||||
presentation/support/response/ApiResult.java
|
||||
```
|
||||
|
||||
**왜 좋은가:**
|
||||
|
||||
- HTTP 응답 구조는 presentation 소유다
|
||||
- 다른 레이어가 알 필요가 없다
|
||||
- 공용으로 빼면 오히려 경계가 흐려진다
|
||||
|
||||
## 좋은 예시 2: common 대신 module API로 노출
|
||||
|
||||
```text
|
||||
order/
|
||||
OrderManagement.java
|
||||
order/spi/
|
||||
package-info.java (@NamedInterface("spi"))
|
||||
OrderLookup.java
|
||||
```
|
||||
|
||||
**왜 좋은가:**
|
||||
|
||||
- 필요한 범위만 공개한다
|
||||
- 전체 common으로 빼지 않고 모듈 API를 좁게 노출한다
|
||||
|
||||
## 좋은 예시 3: 예외적으로 허용 가능한 작은 공용 타입
|
||||
|
||||
```text
|
||||
common/types/NormalizedHost.java
|
||||
```
|
||||
|
||||
**허용 조건:**
|
||||
|
||||
- 여러 모듈이 실제로 사용
|
||||
- framework/business/persistence 의존 없음
|
||||
- 값 기반 타입
|
||||
- 변화 이유가 동일함
|
||||
|
||||
**왜 좋은가:**
|
||||
|
||||
- 진짜 공통 값 의미를 담는다
|
||||
- owning module이 특정되기 어렵다
|
||||
- 경계를 섞지 않는다
|
||||
|
||||
## 나쁜 예시 1: 잡동사니 common
|
||||
|
||||
```text
|
||||
common/
|
||||
StringUtils.java
|
||||
DateUtils.java
|
||||
ErrorUtils.java
|
||||
ValidationUtils.java
|
||||
AuthConstants.java
|
||||
ApiResult.java
|
||||
UserMapper.java
|
||||
```
|
||||
|
||||
**문제:**
|
||||
|
||||
- 소유권이 불명확하다
|
||||
- web/domain/infrastructure가 섞인다
|
||||
- dump zone이 된다
|
||||
|
||||
## 나쁜 예시 2: 경계 회피용 common
|
||||
|
||||
```text
|
||||
common/UserDto.java
|
||||
```
|
||||
|
||||
**문제:**
|
||||
|
||||
- presentation DTO를 공용으로 올려 application/infrastructure도 기대게 만들 수 있다
|
||||
- DTO/Domain/Entity 경계가 무너진다
|
||||
|
||||
## 나쁜 예시 3: premature abstraction common
|
||||
|
||||
```text
|
||||
common/DeadlineHelper.java
|
||||
```
|
||||
|
||||
**문제:**
|
||||
|
||||
- task와 payment가 지금은 비슷해 보여도 미래에 독립 진화할 수 있다
|
||||
- owning module 안에 두는 편이 더 안전할 수 있다
|
||||
@@ -0,0 +1,136 @@
|
||||
# DTO / Domain / Entity separation 예시
|
||||
|
||||
## 좋은 예시 1: request DTO -> command -> domain
|
||||
|
||||
```java
|
||||
public record CreateUserRequest(
|
||||
String email,
|
||||
String password,
|
||||
String name
|
||||
) {}
|
||||
|
||||
public record CreateUserCommand(
|
||||
String email,
|
||||
String password,
|
||||
String name
|
||||
) {}
|
||||
|
||||
public final class UserWebMapper {
|
||||
|
||||
public CreateUserCommand toCommand(CreateUserRequest request) {
|
||||
return new CreateUserCommand(
|
||||
request.email(),
|
||||
request.password(),
|
||||
request.name()
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**왜 좋은가:**
|
||||
|
||||
- 웹 입력 모델과 application 입력 모델이 분리된다
|
||||
- request binding과 business 의미 부여 경계가 생긴다
|
||||
|
||||
## 좋은 예시 2: entity -> domain 분리
|
||||
|
||||
```java
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
public class UserJpaEntity {
|
||||
@Id
|
||||
private Long id;
|
||||
private String email;
|
||||
private String encodedPassword;
|
||||
private String name;
|
||||
protected UserJpaEntity() {}
|
||||
}
|
||||
|
||||
public class User {
|
||||
private final UserId id;
|
||||
private final UserEmail email;
|
||||
private final UserName name;
|
||||
private final EncodedPassword password;
|
||||
|
||||
private User(...) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**왜 좋은가:**
|
||||
|
||||
- JPA 제약과 도메인 의미가 분리된다
|
||||
- domain이 persistence annotation에 오염되지 않는다
|
||||
|
||||
## 좋은 예시 3: domain -> response DTO 분리
|
||||
|
||||
```java
|
||||
public record UserResponse(
|
||||
Long id,
|
||||
String email,
|
||||
String name
|
||||
) {}
|
||||
|
||||
public final class UserResponseMapper {
|
||||
|
||||
public UserResponse toResponse(User user) {
|
||||
return new UserResponse(
|
||||
user.id().value(),
|
||||
user.email().value(),
|
||||
user.name().value()
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**왜 좋은가:**
|
||||
|
||||
- 외부 응답 계약이 명시적이다
|
||||
- domain 전체를 그대로 노출하지 않는다
|
||||
|
||||
## 나쁜 예시 1: entity를 바로 response로 반환
|
||||
|
||||
```java
|
||||
@GetMapping("/{id}")
|
||||
public UserJpaEntity getUser(@PathVariable Long id) {
|
||||
return userRepository.findById(id).orElseThrow();
|
||||
}
|
||||
```
|
||||
|
||||
**문제:**
|
||||
|
||||
- persistence 구조가 외부 계약으로 새어 나간다
|
||||
- 민감정보/지연로딩/관계 구조가 노출될 수 있다
|
||||
- API와 persistence가 강하게 결합된다
|
||||
|
||||
## 나쁜 예시 2: request DTO를 그대로 domain으로 사용
|
||||
|
||||
```java
|
||||
public User createUser(CreateUserRequest request) {
|
||||
return userService.create(request);
|
||||
}
|
||||
```
|
||||
|
||||
**문제:**
|
||||
|
||||
- 웹 입력 모델이 business layer로 직접 흘러간다
|
||||
- validation/binding shape가 domain/application 설계를 오염시킨다
|
||||
|
||||
## 나쁜 예시 3: domain에 JPA/JSON/validation annotation 혼합
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class User {
|
||||
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
@JsonProperty("email")
|
||||
@NotBlank
|
||||
private String email;
|
||||
}
|
||||
```
|
||||
|
||||
**문제:**
|
||||
|
||||
- persistence / serialization / validation / business 의미가 한 타입에 섞인다
|
||||
- 변경 이유가 서로 다른 관심사가 강결합된다
|
||||
@@ -0,0 +1,141 @@
|
||||
# interface 생성 예시
|
||||
|
||||
## 좋은 예시 1: application output port
|
||||
|
||||
```java
|
||||
public interface UserReader {
|
||||
Optional<User> findByEmail(UserEmail email);
|
||||
Optional<User> findById(UserId userId);
|
||||
}
|
||||
|
||||
@Repository
|
||||
public class JpaUserReader implements UserReader {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**왜 좋은가:**
|
||||
|
||||
- application이 persistence 구현을 모른다
|
||||
- 바깥 구현 교체와 테스트 대역 주입이 쉽다
|
||||
- 레이어 경계가 분명하다
|
||||
|
||||
## 좋은 예시 2: 외부 시스템 client contract
|
||||
|
||||
```java
|
||||
public interface VaultTransitClient {
|
||||
String sign(String keyName, byte[] input);
|
||||
PublicKey readPublicKey(String keyName);
|
||||
}
|
||||
```
|
||||
|
||||
**왜 좋은가:**
|
||||
|
||||
- 외부 연동 경계가 분명하다
|
||||
- HttpClient/WebClient/Jackson 세부가 계약에 새지 않는다
|
||||
- fake/stub 구현으로 테스트하기 쉽다
|
||||
|
||||
## 좋은 예시 3: 교체 가능한 정책 객체
|
||||
|
||||
```java
|
||||
public interface PasswordHasher {
|
||||
String hash(String rawPassword);
|
||||
boolean matches(String rawPassword, String encodedPassword);
|
||||
}
|
||||
```
|
||||
|
||||
**왜 좋은가:**
|
||||
|
||||
- 알고리즘 교체 가능성이 실제로 있다
|
||||
- application/domain이 구체 해시 구현을 모른다
|
||||
|
||||
## 좋은 예시 4: 인터페이스 없이 concrete class 유지
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class LoginResponseAssembler {
|
||||
public LoginResponse toResponse(User user, TokenPair tokenPair) {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**왜 좋은가:**
|
||||
|
||||
- 내부 presentation helper일 뿐 계약 경계가 아니다
|
||||
- 구현체 1개, 교체 가치 낮음, 인터페이스 이득 작음
|
||||
- 불필요한 LoginResponseAssemblerImpl을 만들지 않는다
|
||||
|
||||
## 나쁜 예시 1: 의미 없는 Service/Impl 쌍
|
||||
|
||||
```java
|
||||
public interface UserService {
|
||||
User create(CreateUserCommand command);
|
||||
}
|
||||
|
||||
@Service
|
||||
public class UserServiceImpl implements UserService {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**문제:**
|
||||
|
||||
- 실제 경계/교체/테스트 seam 의미가 약하다
|
||||
- 타입만 늘고 추상화 이득이 거의 없다
|
||||
- “관성적인 인터페이스”에 가깝다
|
||||
|
||||
## 나쁜 예시 2: 기술 세부를 계약에 노출
|
||||
|
||||
```java
|
||||
public interface UserClient {
|
||||
ResponseEntity<String> getUser(String id);
|
||||
}
|
||||
```
|
||||
|
||||
**문제:**
|
||||
|
||||
- Spring Web 타입이 계약에 박힌다
|
||||
- 호출자가 구현 기술에 묶인다
|
||||
|
||||
**개선:**
|
||||
|
||||
- 도메인/애플리케이션에 더 맞는 결과 타입으로 계약 정의
|
||||
|
||||
## 나쁜 예시 3: 여러 책임을 한 인터페이스에 몰아넣기
|
||||
|
||||
```java
|
||||
public interface UserManager {
|
||||
User findUser(...);
|
||||
User saveUser(...);
|
||||
void sendEmail(...);
|
||||
String issueToken(...);
|
||||
}
|
||||
```
|
||||
|
||||
**문제:**
|
||||
|
||||
- 하나의 역할이 아니다
|
||||
- 호출자마다 일부만 필요할 가능성이 높다
|
||||
- 응집도가 낮다
|
||||
|
||||
## 나쁜 예시 4: 조기 추상화
|
||||
|
||||
```java
|
||||
public interface DeadlineService {
|
||||
void setDeadline(...);
|
||||
}
|
||||
|
||||
public class TaskDeadlineService implements DeadlineService { ... }
|
||||
|
||||
public class PaymentDeadlineService implements DeadlineService { ... }
|
||||
```
|
||||
|
||||
**문제:**
|
||||
|
||||
- 지금은 비슷해 보여도 미래에 다르게 진화할 수 있다
|
||||
- 아직 공통 계약이 자연스러운지 검증되지 않았다
|
||||
|
||||
**개선 방향:**
|
||||
|
||||
- 충분한 공통성/경계 필요가 생길 때까지 분리된 concrete class 유지
|
||||
@@ -0,0 +1,152 @@
|
||||
# mapper separation 예시
|
||||
|
||||
## 좋은 예시 1: web request -> command 매핑
|
||||
|
||||
```java
|
||||
public final class UserWebMapper {
|
||||
|
||||
public CreateUserCommand toCommand(CreateUserRequest request) {
|
||||
return new CreateUserCommand(
|
||||
request.email(),
|
||||
request.password(),
|
||||
request.name()
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**왜 좋은가:**
|
||||
|
||||
- HTTP request 구조를 application command 구조로만 번역한다
|
||||
- 비즈니스 정책을 결정하지 않는다
|
||||
|
||||
## 좋은 예시 2: domain -> response DTO 매핑
|
||||
|
||||
```java
|
||||
public final class UserResponseMapper {
|
||||
|
||||
public UserResponse toResponse(User user) {
|
||||
return new UserResponse(
|
||||
user.id().value(),
|
||||
user.email().value(),
|
||||
user.name().value()
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**왜 좋은가:**
|
||||
|
||||
- 응답 계약만 만든다
|
||||
- repository/service 호출이 없다
|
||||
|
||||
## 좋은 예시 3: persistence entity -> domain 매핑 분리
|
||||
|
||||
```java
|
||||
public final class UserPersistenceMapper {
|
||||
|
||||
public User toDomain(UserJpaEntity entity) {
|
||||
return User.restore(
|
||||
entity.getId(),
|
||||
entity.getEmail(),
|
||||
entity.getName(),
|
||||
entity.getEncodedPassword()
|
||||
);
|
||||
}
|
||||
|
||||
public UserJpaEntity toEntity(User user) {
|
||||
return new UserJpaEntity(
|
||||
user.id().value(),
|
||||
user.email().value(),
|
||||
user.name().value(),
|
||||
user.password().encodedValue()
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**왜 좋은가:**
|
||||
|
||||
- persistence 구조와 domain 구조를 별도 경계에서 번역한다
|
||||
- JPA 세부가 domain으로 직접 새지 않는다
|
||||
|
||||
## 좋은 예시 4: update mapping을 명시적으로 분리
|
||||
|
||||
```java
|
||||
public interface UserPersistenceMapper {
|
||||
|
||||
UserJpaEntity toNewEntity(User user);
|
||||
|
||||
void updateEntity(User user, UserJpaEntity target);
|
||||
}
|
||||
```
|
||||
|
||||
**왜 좋은가:**
|
||||
|
||||
- 생성과 수정의 계약이 다름을 드러낸다
|
||||
- side effect가 있는 매핑을 명시한다
|
||||
|
||||
## 나쁜 예시 1: 매퍼에서 repository 호출
|
||||
|
||||
```java
|
||||
public final class UserMapper {
|
||||
|
||||
private final RoleRepository roleRepository;
|
||||
|
||||
public User toDomain(UserRequest request) {
|
||||
Role role = roleRepository.findByName(request.roleName()).orElseThrow();
|
||||
return User.create(request.email(), role);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**문제:**
|
||||
|
||||
- 매퍼가 번역을 넘어 DB 조회까지 한다
|
||||
- 테스트와 책임 분리가 어려워진다
|
||||
|
||||
**개선:**
|
||||
|
||||
- 호출자가 Role을 먼저 준비해서 전달한다
|
||||
|
||||
## 나쁜 예시 2: 매퍼에서 비즈니스 규칙 결정
|
||||
|
||||
```java
|
||||
public UserStatus toStatus(UserRequest request) {
|
||||
if (request.provider().equals("google")) {
|
||||
return UserStatus.ACTIVE;
|
||||
}
|
||||
return UserStatus.PENDING;
|
||||
}
|
||||
```
|
||||
|
||||
**문제:**
|
||||
|
||||
- 상태 결정 정책이 매퍼에 숨어 있다
|
||||
- 단순 구조 변환이 아니라 비즈니스 의미를 만든다
|
||||
|
||||
**개선:**
|
||||
|
||||
- status 결정은 application/domain 정책으로 이동
|
||||
|
||||
## 나쁜 예시 3: web + persistence + integration를 한 매퍼에 몰아넣기
|
||||
|
||||
```java
|
||||
public final class UserMapper {
|
||||
CreateUserCommand toCommand(CreateUserRequest request) { ... }
|
||||
UserJpaEntity toEntity(User user) { ... }
|
||||
ExternalUserPayload toPayload(User user) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**문제:**
|
||||
|
||||
- 경계가 섞인다
|
||||
- 변경 이유가 달라 함께 진화하기 어렵다
|
||||
|
||||
**개선:**
|
||||
|
||||
- UserWebMapper
|
||||
- UserPersistenceMapper
|
||||
- UserExternalMapper
|
||||
- 로 분리
|
||||
@@ -0,0 +1,127 @@
|
||||
# port abstraction 예시
|
||||
|
||||
## 좋은 예시 1: outbound port를 application이 소유
|
||||
|
||||
```java
|
||||
public interface UserReader {
|
||||
Optional<User> findByEmail(UserEmail email);
|
||||
Optional<User> findById(UserId userId);
|
||||
}
|
||||
|
||||
@Repository
|
||||
public class JpaUserReader implements UserReader {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**왜 좋은가:**
|
||||
|
||||
- application이 persistence 기술을 모른다
|
||||
- 코어가 필요한 조회 능력만 계약으로 드러난다
|
||||
- adapter만 JPA를 안다
|
||||
|
||||
## 좋은 예시 2: inbound port를 use case 계약으로 사용
|
||||
|
||||
```java
|
||||
public interface LoginUseCase {
|
||||
LoginResult login(LoginCommand command);
|
||||
}
|
||||
|
||||
@RestController
|
||||
class LoginController {
|
||||
private final LoginUseCase loginUseCase;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**왜 좋은가:**
|
||||
|
||||
- controller가 구현체보다 use case 계약에 의존한다
|
||||
- HTTP 세부와 비즈니스 흐름이 분리된다
|
||||
|
||||
## 좋은 예시 3: external API 경계 포트
|
||||
|
||||
```java
|
||||
public interface TokenSigner {
|
||||
Signature sign(SigningRequest request);
|
||||
}
|
||||
|
||||
public class VaultTokenSigner implements TokenSigner {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**왜 좋은가:**
|
||||
|
||||
- 포트는 “서명한다”는 능력만 표현한다
|
||||
- HTTP, JSON, Vault path/header는 adapter 구현으로 숨긴다
|
||||
|
||||
## 좋은 예시 4: 하나의 포트에 여러 adapter 가능
|
||||
|
||||
```java
|
||||
public interface RateRepository {
|
||||
BigDecimal findDiscountRate(Money amount);
|
||||
}
|
||||
|
||||
public class InMemoryRateRepository implements RateRepository { ... }
|
||||
|
||||
public class JdbcRateRepository implements RateRepository { ... }
|
||||
```
|
||||
|
||||
**왜 좋은가:**
|
||||
|
||||
- 테스트와 운영 구현이 같은 계약을 공유한다
|
||||
- 포트는 기술 수와 무관하게 같은 대화를 표현한다
|
||||
|
||||
## 나쁜 예시 1: 기술 타입이 새는 포트
|
||||
|
||||
```java
|
||||
public interface UserApiPort {
|
||||
ResponseEntity<String> getUser(String id);
|
||||
}
|
||||
```
|
||||
|
||||
**문제:**
|
||||
|
||||
- HTTP 세부가 코어 계약으로 올라온다
|
||||
- 비즈니스 의미가 아니라 transport 형식이 중심이 된다
|
||||
|
||||
## 나쁜 예시 2: adapter 편의 중심 포트
|
||||
|
||||
```java
|
||||
public interface DatabasePort {
|
||||
String query(String sql);
|
||||
}
|
||||
```
|
||||
|
||||
**문제:**
|
||||
|
||||
- 코어가 SQL/DB 기술 세부를 알게 된다
|
||||
- “무엇을 원하나”가 아니라 “어떻게 하냐”를 말한다
|
||||
|
||||
## 나쁜 예시 3: 너무 범용적인 outbound port
|
||||
|
||||
```java
|
||||
public interface ExternalSystemPort {
|
||||
Object execute(Object input);
|
||||
}
|
||||
```
|
||||
|
||||
**문제:**
|
||||
|
||||
- 역할이 불명확하다
|
||||
- 타입 안정성과 계약 의미가 없다
|
||||
- 여러 외부 시스템 책임을 한 곳에 섞기 쉽다
|
||||
|
||||
## 나쁜 예시 4: 내부 helper까지 포트화
|
||||
|
||||
```java
|
||||
public interface EmailNormalizerPort {
|
||||
String normalize(String raw);
|
||||
}
|
||||
```
|
||||
|
||||
**문제:**
|
||||
|
||||
- 외부 경계가 아니라 내부 로직 detail이다
|
||||
- 포트 추상화 비용이 이득보다 크다
|
||||
@@ -0,0 +1,136 @@
|
||||
# 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);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**문제:**
|
||||
|
||||
- 값 객체가 외부 의존과 오케스트레이션을 떠안는다
|
||||
- 순수한 값 의미 타입이 아니다
|
||||
Reference in New Issue
Block a user