기존에 구현했던 쇼핑몰은 서비스 계층에서 대부분의 비즈니스 로직을 처리하게 만들었다. 이를 트랜잭션 스크립트 패턴이라고 하는데, 사실 쇼핑몰을 구성하는 엔티티에는 엔티티 자체를 설명해주는 속성값만 명시해놨을 뿐 그 어떤 생성자나 메서드를 적극적으로 활용하지 못했다. 사실 서비스 계층에서 처리하면 아무런 문제가 없지만, 서비스 로직을 재사용하기엔 코드의 복잡성이 크고, 가독성이 좋지 못하다는 느낌을 받았다.
그러다 김영한 강사님의 [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;
}
}
}
끝으로..
주문 로직의 큰 틀은 바뀐건 없지만 도메인 모델 패턴을 이용했을 때 코드의 흐름이 나름 명확해지고, 이해하기가 수월해졌다고 생각한다. 사실 위의 코드를 리팩터링한 것은 빙산의 일각에 불과하다. 잘 설계된 도메인 모델 패턴을 위해서는 프로젝트 설계시에 객체의 역할을 명확히 판별하고, 관계를 알맞게 정립해야 하기 때문에 굉장히 까다롭다고 한다. 이번 기회를 계기로 도메인 주도 설계에 대해서 더 배우고 싶다는 생각을 했고, 실제 프로젝트에 적용하기 위해서 노력할 것이다. 끝으로 미리 작성된 테스트 코드로 테스트한 결과 모두 파란불이 켜졌당.
'Projects > 쇼핑몰 프로젝트' 카테고리의 다른 글
AWS EC2를 이용한 Springboot + Mysql 서비스 배포 (0) | 2023.03.31 |
---|---|
Can not issue data manipulation statements with executeQuery() 에러 해결 (0) | 2023.03.30 |
[JUnit] Spring Security 로그인 테스트 (0) | 2023.02.27 |
[장바구니] 장바구니 기능구현(상품추가, 수량변경) (0) | 2023.01.13 |
[주문] 아임포트 결제 & 결제 성공 시 주문 로직 구현 (4) | 2023.01.09 |
댓글