쿼리를 통해 보는 CASCADE와 orphanRemoval 비교

최인준·2023년 12월 13일
6
post-thumbnail

CASCADE? orphanRemoval?

처음에 JPA를 공부할 때 헷갈렸었던 부분이 있었다.

객체간의 연관관계에 있어 cascade 옵션과 orphanRemoval 옵션에 대해 공부할 때 각자의 개념에 대해서는 쉽게 이해할 수 있었다.

하지만 CASCADE와 orphanRemoval의 명확한 경계를 나 스스로 설명하기엔 조금 막히는 부분이 있었다.

애매한 부분에 대해 공부 할 때는 쿼리를 통해 눈으로 직접 확인하는 것이 개념에 대해 명확히 이해가 쉬운 것 같다.

이 둘에 대한 공통점과 차이점을 개념적으로 알고 있다 하더라도 실제 나가는 쿼리를 보지 않았다면 누구나 찝찝할 것이다.

그래서 혹시나 이해가 애매한 분들을 위해 내가 쿼리 비교로 공부했던 것을 공유하고 싶어 포스팅하게 되었다.


공통점과 차이점

먼저 공통점과 차이점을 알기 전에 알아야 할 부분이 있다.

orphanRemoval 옵션을 사용하기 위해서는 CASCADE 속성을 정의해야 한다. JPA 표준에 따르면 CASCADE와 orphanRemoval은 서로 독립적이지만 Hibernate 구현체에서는 CASCADE 속성에 의존적이다.

실제로 orphanRemoval 옵션만 정의하고 테스트 할 시 옵션에 관한 쿼리가 나가지 않는다 ‼️

이제 실제 예제 코드를 실행해보면서 쿼리를 통해 둘을 비교해 볼 것이다.

CASCADE 옵션은 ALL로 설정할 것이다.


엔티티

먼저 예제를 위해 간단한 엔티티 2개를 준비했다. Member 객체와 Order 객체다.

Member와 Order는 1:N 관계이다.

Member.java

@Entity
@Getter
@NoArgsConstructor
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;
    private String name;
    private String email;

    @OneToMany(mappedBy = "member")
    private List<Order> order = new ArrayList<>();

    @Builder
    public Member(String name, String email){
        this.name = name;
        this.email = email;
    }
}

Order.java

@Entity
@NoArgsConstructor
@Table(name = "orders")
@Getter
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "order_id")
    private Long id;

    private String productName;

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

    @Builder
    public Order(String productName){
        this.productName = productName;
    }
    public void setMember(Member member){ //연관관계 설정
        this.member = member;
        member.getOrder().add(this);
    }
    public void deleteMember(Member member){ // 연관관계 해제
        member.getOrder().remove(this);
        this.member = null;
    }

→ Order 내의 두 메소드는 연관관계 설정에 관한 메소드이다. 실습에 필요하여 정의해두었다.

예제 데이터

예제 데이터로 두명의 member가 있고 각 member 당 order가 2개 있는 상태로 설정을 해두었다.

공통점

orphanRemoval이 없을 때

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

먼저 CASCADE 옵션만 걸려있고 orphanRemoval 옵션이 걸려있지 않은 상태(false)를 보자.

개념적으로 둘의 공통점은 부모 엔티티가 삭제 됐을 때 자식 엔티티가 삭제된다는 것이다.

테스트 코드는 다음과 같다.

@Test
void remove(){
    //given
    Member member = memberRepository.findById(1L).orElseThrow();
    //when
    memberRepository.delete(member);

    entityManager.flush();
}

id가 1인 회원을 삭제했다. 그럼 이 회원에게 연관관계가 걸려있는 order 두개에 대한 delete 쿼리와 회원에 대한 delete 쿼리가 나갈 것을 기대할 수 있다.

그럼 실행된 결과 쿼리를 살펴보자.

예상했던 대로 총 3개의 delete 쿼리가 나간 것을 볼 수 있다.

orphanRemoval이 있을 때

@OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Order> order = new ArrayList<>();

orphanRemoval 옵션을 걸고 위와 동일한 테스트 코드를 실행했을 때, 동일한 쿼리가 나간다.

공통점에 대한 개념은 누구나 쉽게 알고 있다. 우리의 목적은 차이점에 대한 쿼리 분석이다.

차이점

차이점에 대한 내용도 크게 어렵지 않다.

개념상으로 둘의 차이점은 연관관계 자체에 있다. member와 order에게 연관관계가 걸려있는 상태에서 둘의 연관관계를 해제하는 상황을 생각해보자.

CASCADE는 말 그대로 영속성 전이와 관련된 옵션이기 때문에 둘의 연관관계가 해제 된다고 해서 아무런 일도 일어나지 않는다.

orphanRemoval 옵션을 걸어주는 것은 JPA가 고아객체에 대해 알아서 삭제를 할 수 있도록 해주는 것이다.

즉, 차이점은 연관관계를 해제했을 때의 발생하는 쿼리에 있다.

CASCADE 옵션만 있을 때는 아무런 삭제 쿼리도 나가지 않고 orphanRemoval 옵션이 추가적으로 걸려 있을 때는 연관관계 해제 시 부모 객체(여기선 member)와의 연관관계가 끊어진 자식 고아객체(order)에 대한 삭제쿼리가 나간다.

orphanRemoval 옵션이 없는 상태에서 연관관계 해제

CASCADE 옵션만 있는 상태에서 연관관계가 맺어진 두 객체에 대한 연관관계를 해제해보자.

테스트 코드는 다음과 같다.

@Test
void removeMapping(){
    //given
    Order order = orderRepository.findById(1L).orElseThrow();
    Member member = memberRepository.findById(1L).orElseThrow();
    //when
    order.deleteMember(member);

    entityManager.flush();
}

deleteMember() 메소드는 위의 Order Entity에 있던 연관관계를 해제하는 메소드이다.

위의 개념에서 이해한대로라면 CASCADE 옵션만 걸려있는 상태에서는 연관관계를 해제했다고 해서 delete 쿼리가 나가지 않을 것이다. 그럼 쿼리를 살펴보자.

보이는 것과 같이 해당 order에 대해 member를 null로 설정하는 연관관계 해제 update 쿼리만 나가고 끝난다.

delete 쿼리는 하나도 발생하지 않은 것이다.

이제 orphanRemoval 옵션을 걸어주어 고아객체에 대한 delete 쿼리가 나가는 것을 직접 확인해보자.

orphanRemoval 옵션이 있는 상태에서 연관관계 해제

@OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Order> order = new ArrayList<>();

이제 orphanRemoval 옵션을 걸어주었으니 부모와의 연관관계를 해제했을 때,

order에 대한 delete 쿼리가 발생해야 한다. 바로 위와 동일한 테스트 코드를 실행켜보았다.

우리가 이해한대로 자식 객체에 대한 delete 쿼리가 발생하였다.

위와 다른점은 위에서는 update쿼리가 나가고 여기선 delete쿼리가 발생했다는 것이다.

JPA는 내부적으로 연관관계 해제를 인지하고 orphanRemoval 옵션이 안 걸려있다면 update 쿼리를 발생시키고 옵션이 걸려있다면 update 쿼리를 발생시키지 않고 바로 고아객체에 대한 delete 쿼리를 발생시킨다.

마무리

이렇게 쿼리를 통해 눈으로 공통점과 차이점을 비교해보았다.

머리로만 이해하고 사용한다면 문제가 발생했을 때 어디서 문제가 발생했고 어떤 쿼리가 어떻게 발생했는 지 파악하기 어려울 수 있다.

하지만 이렇게 이해한 개념을 쿼리를 통해 실습하면 확실한 목적을 가진 상태에서 이 두 옵션을 적용시킬 수 있다.

JPA는 우리 대신 쿼리를 발생시켜주는 대신 어떤식으로 발생하는 지에 대한 이해가 확실히 필요한 것 같다 ‼️

2개의 댓글

comment-user-thumbnail
2023년 12월 26일

jpa 강의 들으면서 뭐가 다른거지라고 생각했던 애매한 부분을 명확히 설명해주셔서 도움이 되었습니다. 감사합니다

1개의 답글