Updated:

 지금까지 팀프로젝트를 진행하면 다음과 같은 상황이 자주 발생하였다.

  • Entity 하나가 모든 것의 중심이 된다.
  • Service 코드에 Repository가 잔뜩 있다.
  • 하나의 메서드에 여러 기능이 들어가 있다.
@Entity
@Table(name = "p_user")
public class User extends BaseEntity {

    @Id @UuidGenerator
    private UUID id;
    private String loginId;
    private String password;
    private String email;
    private String name;
    private String phone;

    @Enumerated(EnumType.STRING)
    private UserStatus status = UserStatus.ACTIVE;

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
    private List<UserRole> userRoles = new ArrayList<>();

    // 여기까지는 괜찮아 보이지만...
    // 만약 주문, 리뷰, 장바구니, 배송지까지 User가 직접 들고 있다면?
    @OneToMany(mappedBy = "user")
    private List<Order> orders = new ArrayList<>();

    @OneToMany(mappedBy = "user")
    private List<Review> reviews = new ArrayList<>();

    @OneToMany(mappedBy = "user")
    private List<Cart> carts = new ArrayList<>();

    @OneToMany(mappedBy = "user")
    private List<UserAddress> addresses = new ArrayList<>();

    @OneToMany(mappedBy = "user")
    private List<Store> stores = new ArrayList<>();  // OWNER인 경우

    // getter, setter 수십 개...
}
@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {

    private final OrderRepository orderRepository;
    private final UserRepository userRepository;
    private final StoreRepository storeRepository;
    private final ProductRepository productRepository;
    private final UserAddressRepository userAddressRepository;
    private final PaymentService paymentService;
    private final OrderPaymentProcessor orderPaymentProcessor;

    // ...
}
@Override
@Transactional
public CreateOrderResponse createOrder(UUID userId, CreateOrderRequest request) {

    // 1. 유저 조회
    User user = userRepository.findById(userId)
        .orElseThrow(() -> new IllegalArgumentException("USER_NOT_FOUND"));

    // 2. 가게 존재 확인
    Store store = storeRepository.findById(request.getStoreId())
        .orElseThrow(() -> new IllegalArgumentException("STORE_NOT_FOUND"));

    // 3. 주문번호 생성
    String orderNo = "ORD-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();

    // 4. 총 주문 금액 계산 — 옵션 가격까지 직접 합산
    int totalAmount = request.getItems().stream()
        .mapToInt(item -> {
            int optionSum = 0;
            if (item.getOptions() != null) {
                optionSum = item.getOptions().stream()
                    .mapToInt(opt -> opt.getExtraPrice() == null ? 0 : opt.getExtraPrice())
                    .sum();
            }
            return (item.getUnitPrice() + optionSum) * item.getQuantity();
        })
        .sum();

    // 5. 배송지 확인
    UserAddress deliveryAddress = null;
    if (request.getDeliveryAddressId() != null) {
        deliveryAddress = userAddressRepository
            .findByIdAndUserId(request.getDeliveryAddressId(), userId)
            .orElseThrow(() -> new IllegalArgumentException("DELIVERY_ADDRESS_NOT_FOUND"));
    }

    // 6. 주문 생성
    Order order = new Order(
        user, store, deliveryAddress,
        orderNo, totalAmount, request.getRequestMemo()
    );

    // 7. 주문 항목 생성 — 상품 조회 + 옵션 매핑 + 연관관계 설정
    for (CreateOrderRequest.CreateOrderItemRequest itemReq : request.getItems()) {
        Product product = productRepository.findById(itemReq.getProductId())
            .orElseThrow(() -> new IllegalArgumentException("PRODUCT_NOT_FOUND"));

        OrderItem orderItem = new OrderItem(
            product, itemReq.getProductName(),
            itemReq.getUnitPrice(), itemReq.getQuantity()
        );

        if (itemReq.getOptions() != null) {
            for (CreateOrderRequest.CreateOrderItemOptionRequest optReq : itemReq.getOptions()) {
                OrderItemOption option = new OrderItemOption(
                    optReq.getOptionName(), optReq.getOptionItemName(),
                    optReq.getExtraPrice()
                );
                orderItem.addOption(option);
            }
        }

        order.addItem(orderItem);
    }

    Order savedOrder = orderRepository.save(order);

    // 8. 결제 엔티티 생성
    paymentService.createPayment(
        CreatePaymentCommand.of(savedOrder, savedOrder.getTotalAmount())
    );

    return CreateOrderResponse.from(savedOrder);
}

 위와 같은 코드는 “트랜잭션 스크립트” 패턴이라고 한다. 하나의 요청을 처리하는 절차를 하나의 메서드에 스크립트처럼 순서대로 나열하는 방식이다. 이는 직관적이지만, 비즈니스가 복잡해지는 순간 한계에 부딪힌다.
 위 코드에서 “최소 주문 금액 검증 추가해 주세요”, “주문 거절 시 결제 환불해 주세요”, “주문 금액 미리보기 만들어 주세요”와 같이 요청을 받게 되면 createOrder가 더 길어지거나 비슷한 코드를 복사해 붙여넣게된다.
 이는 트랜잭션 스크립트의 근본적인 문제이다. 비즈니스 규칙이 “코드의 절차” 안에 녹아 있어서, 규칙만 따로 꺼내 쓸 수가 없다.

DDD

 앞에서 본 주문 생성 기능을 DDD 관점에서 설계하면 어떻게 달라지는지 알아본다.

MSA

댓글남기기