Projects/쇼핑몰 프로젝트

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

젊은오리 2023. 3. 3. 03:13
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