주문(Order) 시스템을 예로 들자. Order 엔티티에는 id, customerId, items,
totalAmount, status, createdAt 필드가 있다.
이 엔티티를 "주문 생성" API의 요청 모델로도 쓴다면 어떤 일이 생기는지 살펴보자.
class Order {
Long id; // DB가 생성 — 요청 시엔 없어야 함
Long customerId;
List<Item> items;
BigDecimal totalAmount; // 서버가 계산 — 요청 시엔 없어야 함
OrderStatus status; // 서버가 설정 — 요청 시엔 없어야 함
LocalDateTime createdAt; // 서버가 설정 — 요청 시엔 없어야 함
}
이 Order를 주문 생성, 주문 조회, 배송지 변경 세 API에 모두 쓴다면 각 상황에서 "있어야 하는 필드"가 다 다르다.
customerId, items만 필요id, totalAmount, status, createdAt은 떠돌이 데이터 (있으면 안 되거나, 있어도 무시해야 함)deliveryAddress만 필요public Order createOrder(Order request) {
// 조건문 1: 클라이언트가 id를 임의로 넣으면?
if (request.getId() != null) {
throw new IllegalArgumentException("id는 서버가 생성합니다");
// 또는 그냥 무시하고 덮어씀
}
// 조건문 2: 클라이언트가 금액을 조작하면?
if (request.getTotalAmount() != null) {
log.warn("totalAmount는 무시합니다: {}", request.getTotalAmount());
}
// 그리고 결국 서버에서 다시 계산
request.setTotalAmount(calculateTotal(request.getItems()));
// 조건문 3: 클라이언트가 status를 넣으면?
if (request.getStatus() != null) {
throw new IllegalArgumentException("status는 직접 설정할 수 없습니다");
}
request.setStatus(OrderStatus.PENDING); // 항상 덮어씀
// 조건문 4: 클라이언트가 createdAt을 넣으면?
if (request.getCreatedAt() != null) {
log.warn("createdAt은 무시합니다");
}
request.setCreatedAt(LocalDateTime.now()); // 항상 덮어씀
// 실제 로직은 단 한 줄...
return orderRepository.save(request);
}
이 메서드에서 실제 업무 로직은 맨 마지막 한 줄이다. 나머지는 전부 "있으면 안 되는 데이터를 방어하는" 조건문이다. 이것이 떠돌이 데이터가 만들어낸 조건문 폭발이다.
더 큰 문제는 이 조건문들이 다른 서비스, 다른 레이어에도 퍼진다는 것이다. 배송지 변경 API를 처리하는 메서드에서도 items가 왜 있는지, totalAmount를 신뢰해야 하는지 계속 의심해야 한다.
// 요청 모델: 필요한 것만 딱 담는다
class CreateOrderRequest {
Long customerId; // 필요한 것만
List<Item> items; // 필요한 것만
// id? totalAmount? status? createdAt? → 아예 없음
}
// 응답 모델: 보여줄 것만 담는다
class CreateOrderResponse {
Long orderId;
OrderStatus status;
BigDecimal totalAmount;
LocalDateTime createdAt;
}
// 서비스: 이제 조건문이 사라진다
public CreateOrderResponse createOrder(CreateOrderRequest request) {
Order order = new Order(
request.getCustomerId(),
request.getItems()
// id, totalAmount, status, createdAt은 엔티티 내부에서 결정
);
Order saved = orderRepository.save(order);
// 응답 모델로 변환해서 반환
return new CreateOrderResponse(saved.getId(), saved.getStatus(), ...);
}
요청 모델에 처음부터 필요 없는 필드가 존재하지 않으니 방어 조건문 자체가 생길 이유가 없다. 코드가 단순해지고, "이 값을 신뢰해도 되나?"라는 의심도 사라진다.
왼쪽은 Order 하나를 세 가지 용도에 우겨넣는 구조 — 각 API에서 필요 없는 필드들이 떠돌이가 되어 조건문을 강제한다. 오른쪽은 각 유즈케이스에 맞는 모델을 따로 정의해 엔티티를 오염 없이 유지하는 구조다.