핵심 답변
Entity는 모든 상황에 쓸 수 있는 만능 객체처럼 보이지만, 요청/응답 모델로 재사용하면 "이 상황에는 이 필드가 없어야 해"라는 조건문이 서비스 곳곳에 퍼지기 시작한다. 이게 Clean Architecture가 말하는 떠돌이 데이터 문제다.

주문(Order) 시스템을 예로 들자. Order 엔티티에는 id, customerId, items, totalAmount, status, createdAt 필드가 있다.

이 엔티티를 "주문 생성" API의 요청 모델로도 쓴다면 어떤 일이 생기는지 살펴보자.

문제가 생기는 시나리오 — 엔티티를 요청 모델로 쓸 때

Order 엔티티 (문제의 시작)

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만 필요
→ 나머지 모든 필드가 떠돌이 데이터
더 깊이 파고들기 — 조건문이 쌓이는 과정

🚨 Bad: 엔티티를 요청 모델로 재사용

문제 있는 코드
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를 신뢰해야 하는지 계속 의심해야 한다.

✅ Good: 용도별 전용 모델 분리

Clean Architecture 방식
// 요청 모델: 필요한 것만 딱 담는다
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 (만능 객체) id customerId items totalAmount ← 떠돌이 status ← 떠돌이 createdAt ← 떠돌이 deliveryAddress ← 떠돌이 POST /orders GET /orders/:id PATCH /delivery if (id != null) throw ... if (totalAmount != null) ignore ... ✅ 용도별 전용 모델 분리 CreateOrderRequest customerId ✓ items ✓ // 딱 필요한 것만 UpdateDeliveryReq orderId ✓ newAddress ✓ // 딱 필요한 것만 Order (순수 도메인) id, customerId, items totalAmount, status createdAt, deliveryAddress calculateTotal(), changeStatus() 조건문 없음 각 모델이 의도를 명확히 표현

왼쪽은 Order 하나를 세 가지 용도에 우겨넣는 구조 — 각 API에서 필요 없는 필드들이 떠돌이가 되어 조건문을 강제한다. 오른쪽은 각 유즈케이스에 맞는 모델을 따로 정의해 엔티티를 오염 없이 유지하는 구조다.

함께 알면 좋은 개념