DDD
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 관점에서 설계하면 어떻게 달라지는지 알아본다.
댓글남기기