코딩과 결혼합니다

[JPA] 아키텍처 패턴(+ 디자인 패턴) 본문

2세/JPA

[JPA] 아키텍처 패턴(+ 디자인 패턴)

코딩러버 2024. 2. 15. 16:00
728x90

  이전에 아키텍처 패턴에 대한 질문을 받았었다. 난 이 개념에 대해 모르고 있어 알고 있는 디자인 패턴을 설명하는 부끄러운 실수를 했었다. 강의를 들으며 그때 물어봤던 게 이 패턴이었구나 깨닫는다. 오늘도 새로운 지식을 습득하며 한층 성장한다!


디자인 패턴

소프트웨어 설계 시 특정 맥락에서 자주 발생하는 문제들을 효과적으로 해결하기 위한 방법
생성 패턴, 구조 패턴, 행동 패턴 등으로 분류할 수 있다.

 

https://refactoring.guru/ko/design-patterns

 

디자인 패턴들

 

refactoring.guru

 

아키텍처 패턴

주어진 상황에서의 소프트웨어 아키텍처에서 일반적으로 발생하는 문제점들에 대한 일반화되고 재사용 가능한 솔루션이다. 아키텍처 패턴은 소프트웨어 디자인 패턴과 유사하지만 더 큰 범주에 속한다.

10가지의 일반적인 패턴들이 있지만 오늘은 [도메인 모델 패턴], [트랜잭션 스크립트 패턴]에 대해서 알아보고자 한다.

 

도메인 모델 패턴

비즈니스 로직을 객체 지향적으로 모델링한다. 비즈니스 로직이 복잡하고, 비즈니스 규칙이 많거나 변경되는 경우에 적합하다.
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OrderItem {

    @Id
    @GeneratedValue
    @Column(name = "order_item_id")
    private Long id;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "item_id")
    private Item item;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "order_id")
    private Order order;

    private int orderPrice;
    private int count;

    //==생성 메서드==//
    public static OrderItem createOrderItem(Item item, int orderPrice, int count) {
        OrderItem orderItem = new OrderItem();
        orderItem.setItem(item);
        orderItem.setOrderPrice(orderPrice);
        orderItem.setCount(count);

        item.removeStock(count);
        return orderItem;
    }

    //==비즈니스 로직==//
    public void cancel() {
        getItem().addStock(count);
    }

    //==조회 로직==//

    /**
     * 주문 상품 전체 가격 조회
     */
    public int getTotalPrice() {
        return getOrderPrice() * getCount();
    }
}

 

이 코드를 보면 비즈니스 로직을 Entity에 구현하고 있다.

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class OrderService {

    private final OrderRepository orderRepository;
    private final MemberRepository memberRepository;
    private final ItemRepository itemRepository;

    /**
     * 주문
     */
    @Transactional
    public Long order(Long memberId, Long itemId, int count) {

        //엔티티 조회
        Member member = memberRepository.findOne(memberId);
        Item item = itemRepository.findOne(itemId);

        //배송조회 정보
        Delivery delivery = new Delivery();
        delivery.setAddress(member.getAddress());

        //주문상품 생성
        OrderItem orderItem = OrderItem.createOrderItem(item, item.getPrice(), count);

        //주문 생성
        Order order = Order.createOrder(member, delivery, orderItem);

        //주문 저장
        orderRepository.save(order);

        return order.getId();
    }

    /**
     * 주문 취소
     */
    public void cancelOrder(Long orderId) {
        //주문 엔티티 조회
        Order order = orderRepository.findOne(orderId);
        //주문 취소
        order.cancel();
    }
}

서비스 계층은 단순히 엔티티에 필요한 요청을 위임하는 역할을 하게 된다.

 

각 도메인 모델이 자신의 역할과 책임에 집중할 수 있으므로 코드의 가독성과 유지보수성이 향상된다. 또한, 비즈니스 로직이 복잡해지더라도 객체 지향의 원칙에 따라 유연하게 대응할 수 있다.

 

하지만 이를 적용하려면 객체지향 설계 원칙과 패턴에 대한 깊은 이해가 필요하며, 초기 개발 비용이 높을 수 있다.


⭐여기서 이러한 문제가 생길 수 있다. 협업을 할 때에 누군가 Entity에 만들어 놓은 비즈니스 로직을 사용하지 않고 직접 객체를 생성하여 set 하여 값을 채우는 방식을 쓸 수도 있는데, 이는 나중에 유지보수를 하기 어려워지게 만든다.

protected OrderItem(){
}

 

 

그러한 상황을 방지하기 위해 이런 식으로 protected 해주어 다른 생성을 모두 막아준다.

JPA는 Protected까지 기본 생성자를 만들 수 있게 허용해 준다고 한다. 좀 더 간단하게 적용하는 방법으로는 롬복의 기본생성자를 만들어주는 어노테이션에 (access = AccessLevel.PROTECTED)를 해주면 된다.

@NoArgsConstructor(access = AccessLevel.PROTECTED)

이렇게 해두면 다른 개발자 팀원이 이 생성자를 쓰면 안 됨을 인지할 수 있다.


트랜잭션 스크립트 패턴

간단한 비즈니스 로직을 처리하는 경우에 적합하다. 하나의 트랜잭션 내에서 모든 작업을 순차적으로 처리하며, 로직이 복잡하게 얽혀있지 않는 경우에 유리하다.
public void createPost(PostRequestDto requestDto, User user, List<MultipartFile> photos) throws IOException {
    List<PostImg> postImgList = null;
    if (photos != null && !photos.isEmpty()) {
        postImgList = postS3Service.uploadPhotosToS3AndCreatePostImages(photos);
    }

    Post post = new Post(requestDto, user);
    postRepository.save(post);

    if (postImgList != null) {
        associatePostImagesWithPost(post, postImgList);
    }
}

이전의 프로젝트에서 내가 사용했던 패턴 방식과 유사해 보인다. 서비스단에서 모든 로직을 순차적으로 처리한다. 

서비스가 복잡해질 때에는 그 관심사에 따라서 서비스 클래스를 분리하여 메서드를 호출해 사용할 수 있게 하였다.

 

코드의 가독성과 유지보수성을 높이는 데 도움이 되었지만, 비즈니스 로직이 복잡해질수록 한계가 드러난다고 한다.

여러 트랜잭션 스크립트에서 중복된 로직이 발생하거나, 하나의 스크립트가 너무 복잡해질 경우에는 도메인 모델 패턴 등 다른 아키텍처 패턴을 고려해 보는 게 좋을 것 같다.