[Spring] 디자인 패턴, 더티 체킹, CasCade

90000e·2023년 11월 18일
2

[Spring]

목록 보기
7/8

디자인 패턴

도메인 모델 패턴

도메인 모델 패턴이란 쉽게말해 엔티티가 비즈니스 로직을 가지고 객체 지향의 특성을 적극 활용하는 것.

@Entity
@Table(name = "orders")
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {

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

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "member_id")
    private Member member;

    @JsonIgnore
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<>();

    @JsonIgnore
    @OneToOne(fetch = LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "delivery_id")
    private Delivery delivery;

    private LocalDateTime orderDate; //주문시간

    @Enumerated(EnumType.STRING)
    private OrderStatus status; //주문상태 [ORDER, CANCEL]

    //==비즈니스 로직==//
    /**
     * 주문 취소
     */
    public void cancel() {
        if (delivery.getStatus() == DeliveryStatus.COMP) {
            throw new IllegalStateException("이미 배송완료된 상품은 취소가 불가능합니다.");
        }

        this.setStatus(OrderStatus.CANCEL);
        for (OrderItem orderItem : orderItems) {
            orderItem.cancel();
        }
    }

    //==조회 로직==//
    /**
     * 전체 주문 가격 조회
     */
    public int getTotalPrice() {
        int totalPrice = 0;
        for (OrderItem orderItem : orderItems) {
            totalPrice += orderItem.getTotalPrice();
        }
        return totalPrice;
    }

}

이렇게 엔티티 클래스에 비즈니스 로직이 존재하는 구조를 말한다.

장점

  • 객체 지향에 기반한 재사용성, 확장성이 좋다.

단점

  • 하나의 도메인 모델을 구축하는데 많은 노력이 필요하다.

트랜잭션 스크립트 패턴

도메인 모델 패턴과 반대로 엔티티에는 비즈니스 로직이 거의 없고 서비스 계층에서 대부분의 비즈니스 로직을 처리하는 것을 트랜잭션 스크립트 패턴이라 한다.

도메인 모델 패턴과 다르게 모든 비즈니스 로직은 Service로 따로 나누어주는 것을 말한다. 나는 이 방법을 주로 사용했다.

장점

  • 구현 방법의 단순함 때문에 구현이 매우 쉽다. 얼마나 모듈화를 잘하느냐에 따라서 높은 효율을 낼 수 있다.

단점

  • 비즈니스 로직이 복잡해질수록 난잡한 코드를 만들게 된다.

이 두가지 방법은 서로 다른 장점이 존재하기 때문에 어느것이 옳고 그르다 라고 판단하기는 힘들다. 각 프로그램마다 유지보수하기 더 쉬운 방법을 판단하고 찾아 진행하는것이 가장 좋은 방법이다.

레퍼런스
https://sudo-minz.tistory.com/152
실전! 스프링 부트와 JPA 활용1(김영한님)

더티체킹

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

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

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

트랜잭션으로 묶인 주문 취소 부분을 보면 주문을 취소했지만 그 어디에도 update 쿼리를 날린 부분이 없다. 오로지 주문 엔티티를 조회하고 주문 취소 코드가 끝이다. 왜 이렇게만 진행해도 되는걸까?

바로 Data JPA를 사용했기 때문이다. Data JPA는 더티 체킹이라는 기능이 존재하는데, 이는 상태 변경 검사이다.

JPA에서는 변화가 있는 모든 엔티티 객체를 DB에 자동으로 업데이트 해준다.

CasCade

JPA 영속성 전이라고 불린다.
부모 엔티티가 영속화될 때 자식 엔티티도 같이 영속화되고, 부모 엔티티가 삭제될 때 자식 엔티티도 삭제되는 등 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 전이되는 것을 의미한다.

@Entity
@Table(name = "orders")
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {

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

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "member_id")
    private Member member;

    @JsonIgnore
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<>();

    @JsonIgnore
    @OneToOne(fetch = LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "delivery_id")
    private Delivery delivery;

    private LocalDateTime orderDate; //주문시간

    @Enumerated(EnumType.STRING)
    private OrderStatus status; //주문상태 [ORDER, CANCEL]
}

Order 엔티티 안에는 orderItems와 delivery가 Cascade.ALL로 설정되어있다. 이 말은 즉슨 Order 엔티티가 저장될 때, 자동으로 orderItems와 delivery 엔티티에 대한 저장 작업을 자동으로 전파한다는 의미이다.

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
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());
        delivery.setStatus(DeliveryStatus.READY);

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

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

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

        return order.getId();
    }
}

이렇게 order에 대한 save만 진행해줘도, 연관 관계 자동저장을 걸어둔 orderItem과 delivery 엔티티도 자동으로 저장된다는 의미이다.

내가 헷갈린 부분

더티 체킹과 cascade에 대해 이해를 제대로 못하고있었다. 더티 체킹은 트랜잭션이 끝나는 시점에 바뀐 엔티티의 값을 모두 자동으로 반영해준다고 생각했고, 그럼 왜 cascade가 필요한건데?? 라는 의문점이 있었다.

공부를 하다보니 casecade는 의존 관계에 대한 자동 저장이였고, 더티체킹은 해당 엔티티에 대한 변경점 저장이였다.

@Entity
@Table(name = "orders")
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {

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

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "member_id")
    private Member member;

    @JsonIgnore
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<>();

    @JsonIgnore
    @OneToOne(fetch = LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "delivery_id")
    private Delivery delivery;

    private LocalDateTime orderDate; //주문시간

    @Enumerated(EnumType.STRING)
    private OrderStatus status; //주문상태 [ORDER, CANCEL]
}

이 코드를 예를들어 만약 더티체킹만을 이용하고, cascade를 설정하지 않았다면, Order 엔티티의 값은 더티체킹으로 값이 수정됐겠지만, OrderItem 엔티티와 Delivery 엔티티에 해당하는 두 부분은 따로 더티체킹이 되지않아 오류가 나게된다. cascade를 쓰지 않았기 때문에 따로 update를 해주어야 한다.

레퍼런스
https://zzang9ha.tistory.com/350
실전! 스프링 부트와 JPA 활용1(김영한님)

profile
내가 복습하려고 쓰는 블로그

2개의 댓글

comment-user-thumbnail
2024년 5월 22일

멋있어요ㅜㅜ

1개의 답글