본문 바로가기
Projects/쇼핑몰 프로젝트

트랜잭션 스크립트 패턴 > 도메인 모델 패턴으로 변환해보기

by 젊은오리 2023. 3. 3.
728x90

기존에 구현했던 쇼핑몰은 서비스 계층에서 대부분의 비즈니스 로직을 처리하게 만들었다. 이를 트랜잭션 스크립트 패턴이라고 하는데, 사실 쇼핑몰을 구성하는 엔티티에는 엔티티 자체를 설명해주는 속성값만 명시해놨을 뿐 그 어떤 생성자나 메서드를 적극적으로 활용하지 못했다. 사실 서비스 계층에서 처리하면 아무런 문제가 없지만, 서비스 로직을 재사용하기엔 코드의 복잡성이 크고, 가독성이 좋지 못하다는 느낌을 받았다.

그러다 김영한 강사님의 [JPA활용] 강의를 수강했고, 엔티티가 비즈니스 로직을 갖고 객체 지향의 특성을 적극 활용하는 도메인 모델 패턴에 대해서 알게 되었다. 도메인 모델을 사용하는 모습을 보고 객체 지향에 기반하여 추후에 재사용, 확장성, 유지보수가 편리할 것 같다는 생각으로,, 쇼핑몰 프로젝트를 리팩토링하기로 결심했다. 

이 글에서는 작성했던 주문 비즈니스 로직을 기존 트랜잭션 스크립트 패턴에서 도메인 모델 패턴으로 최대한 바꿔보는 과정을 서술할 것이다..!

 

1) 들어가기 앞서..

일단 설계한 주문 비즈니스 로직은 다음과 같다. 

  • 비회원인 경우: 주문 -> 배송정보 생성 -> 비회원User 생성 -> 주문상품 생성 -> 주문 생성
  • 회원인 경우(상품페이지에서 결제): 주문 -> 배송정보 생성 -> 회원User 찾기 -> 주문상품 생성 -> 주문 생성
  • 회원인 경우(장바구니페이지에서 결제):  주문 -> 배송정보 생성 -> 회원User 찾기 -> 장바구니를 주문상품으로 복제 -> 주문 생성 -> 장바구니에서 주문된 상품 삭제

 

2) 변경 전

기존의 서비스 계층에서는 다음과 같이 주문로직에 엔티티 간 연관관계를 매핑하는 코드가 섞여있었다. 

또한 주문, 주문상품 엔티티에는 앞서 말했듯이 엔티티 자체를 설명해주는 속성값만 명시해논 상태이다.

OrderService

@Transactional
public Order makeOrder(){
    //배송 객체 생성
    if(비회원){
        //주문 생성
        //주문상품 생성
        
        //주문 - 주문상품 연관관계 매핑
        //주문 - 배송 연관관계 매핑

    }else if(회원){

        if(상세페이지 결제){
            //주문 생성
            //주문상품 생성

            //주문 - 주문상품 연관관계 매핑
            //주문 - 배송 연관관계 매핑
        }else if(장바구니 결제){
            //주문 생성
            //주문상품 생성
            
            //주문 - 주문상품 연관관계 매핑
            //주문 - 배송 연관관계 매핑
        }
    }
}

 

 

3) 변경 후

일단 단순한 주문 로직임에도 가독성이 좋지 않고, 연관관계를 매번 적어줘야 되다 보니 놓치는 경우도 다반사다. 객체 전반에 관련된 모든 기능을 엔티티에 넣어보자.

회원-주문, 주문상품-주문, 배송정보-주문 매핑 메서드를 선언한다. 동시에 주문 생성 메서드도 만들어준다.

Order

//연관 관계 매핑 메서드
public void setUser(User user){
    this.user = user;
    user.getOrders().add(this);
}
public void addOrderItem(OrderItem orderItem){
    orderItemList.add(orderItem);
    orderItem.setOrder(this);
}
public void setDelivery(Delivery delivery){
    this.delivery = delivery;
    delivery.setOrder(this);
}

//주문 생성 메서드
public static Order createOrder(User user, Delivery delivery, List<OrderItem> orderItems) {
    //8자리 주문번호 생성
    Random random = new Random();
    String number = Integer.toString(random.nextInt(8)+1);
    for(int i=0;i<7;i++){
        number += Integer.toString(random.nextInt(9));
    }

    Order order = new Order();
    order.setUser(user);
    order.setOrder_number(number.toString());
    order.setDelivery(delivery);
    int sum = 0;
    for (OrderItem orderItem : orderItems) {
        order.addOrderItem(orderItem);
        sum += orderItem.getTotal_price();
    }
    order.setOrder_status(1);
    order.setOrder_price(sum);
    order.setOrder_product_count(orderItems.size());
    return order;
}

 

주문상품 엔티티에도 마찬가지로 생성 메서드를 만들어 객체를 생성함과 동시에 모든 속성값이 채워지도록 만들어준다.

OrderItem

//주문상품 생성 메서드
public static OrderItem createOrderItem(Product product, int product_price, int quantity){
    OrderItem orderItem = new OrderItem();
    orderItem.setProduct(product);
    orderItem.setTotal_price(product_price * quantity);
    orderItem.setQuantity(quantity);
    return orderItem;
}

 

이제 이를 기반으로 주문 비즈니스 로직에서 구현한 모습이다. 기존의 객체 간 연관관계 매핑 작업을 도메인 역할로 넘겼기 때문에 서비스 계층에서는 생성메서드만을 활용하여 간결하게 작성할 수 있었다.  

OrderService

@Transactional
public Order makeOrder(int userId, int flag, String address, int productId, int amount){

    //delivery 생성
    Delivery delivery = Delivery.createDelivery(address,"배송전");
    deliveryRepository.save(delivery);

    if(userId == 0){ //비회원의 경우
        //비회원 생성
        User user = User.createAnonymous();
        userRepository.save(user);

        //상품 찾기
        Product product = productRepository.findById(productId).orElseThrow(()->{
            return new CustomException("상품을 찾을 수 없습니다.");
        });
        //주문상품 생성
        List<OrderItem> orderItemList = new ArrayList<>();
        OrderItem orderItem = OrderItem.createOrderItem(product,product.getPrice(),amount);
        orderItemList.add(orderItem);
        //주문 생성
        Order order = Order.createOrder(user,delivery,orderItemList);
        orderRepository.save(order);

        return order;
    }else{ //회원의 경우
        //회원 찾기
        User user = userRepository.findById(userId).orElseThrow(()->{
            return new CustomException("회원를 찾을 수 없습니다.");
        });
        if(flag == 0){  //상세페이지일때
            //상품 찾기
            Product product = productRepository.findById(productId).orElseThrow(()->{
                return new CustomException("상품을 찾을 수 없습니다.");
            });
            //주문상품 생성
            List<OrderItem> orderItemList = new ArrayList<>();
            OrderItem orderItem = OrderItem.createOrderItem(product,product.getPrice(),amount);
            orderItemList.add(orderItem);
            //주문 생성
            Order order = Order.createOrder(user,delivery, orderItemList );
            orderRepository.save(order);

            return order;
        }else{  //장바구니일때

            //장바구니 찾기
            List<Cart> cartList = cartRepository.loadCartByUserId(userId);
            List<OrderItem> orderItemList = new ArrayList<>();
            for(Cart cart : cartList){
                //주문상품 생성
                OrderItem orderItem = OrderItem.createOrderItem(cart.getProduct(),cart.getProduct().getPrice(),cart.getProduct_count());
                orderItemList.add(orderItem);

                //장바구니에서는 삭제
                cartRepository.deleteById(cart.getId());
                user.getCarts().remove(cart);
            }
            //주문 생성
            Order order = Order.createOrder(user,delivery,orderItemList);
            orderRepository.save(order);
            return order;
        }
    }
}

 

끝으로..

주문 로직의 큰 틀은 바뀐건 없지만 도메인 모델 패턴을 이용했을 때 코드의 흐름이 나름 명확해지고, 이해하기가 수월해졌다고 생각한다. 사실 위의 코드를 리팩터링한 것은 빙산의 일각에 불과하다. 잘 설계된 도메인 모델 패턴을 위해서는 프로젝트 설계시에 객체의 역할을 명확히 판별하고, 관계를 알맞게 정립해야 하기 때문에 굉장히 까다롭다고 한다. 이번 기회를 계기로 도메인 주도 설계에 대해서 더 배우고 싶다는 생각을 했고, 실제 프로젝트에 적용하기 위해서 노력할 것이다. 끝으로 미리 작성된 테스트 코드로 테스트한 결과 모두 파란불이 켜졌당.

728x90

댓글