본문 바로가기
IT 공부/자바와 웹 애플리케이션

[JPA] -연관관계가 있을 때 영속성 처리법과 cascade

by exdus3156 2024. 1. 24.

JPA 기술은 너무나 편리하지만, 한편으로 신경써야 할 것도 꽤 많다.

특히 테이블 사이의 연관관계가 형성될 때, JpaRepository에서 영속 처리를 하면 연관 관계에 있는 다른 엔티티들은 도대체 어떻게 처리할지 생각해야 한다.

 

1. select는 지연 로딩과 즉시 로딩, 그리고 조인(JOIN)

JpaRepository에서 findById()와 같은 메소드로 엔티티 객체를 가지고 오는 경우, 내부의 엔티티를 어떻게 가져올지 결정해야 한다. 처음 생각해야 할 것이 지연 로딩(LAZY)즉시 로딩(EAGER)이다.

@Entity
public class Reply {
    // ....
    
    @ManyToOne(fetch = FetchType.LAZY)
    private Board board;
    
}

지연 로딩(LAZY)이란, 연결된 객체를 가져오지 않는 것을 말한다. 대신 @Transactional 범위 안에서 session 연결을 유지하며 엔티티를 필요로 하는 순간에만 영속 계층에서 DB 처리를 해준다.

지연 로딩과 관련된 포스팅은 다음 링크에 정리를 했으니 여기서는 생략한다.

 

[JPA] - 영속성 컨텍스트와 지연 로딩(LAZY)

1. 지연 로딩이란? 아래와 같은 아주 간단한 Reply(댓글) Entity 클래스가 있다. @Entity public class Reply { @Id private Long rno; @ManyToOne(fetch = FetchType.LAZY) private Board board; } 댓글(Reply) 엔티티는 Board(게시판) 엔

linocraft.tistory.com

 

즉시 로딩(EAGER)은 지연 로딩과 반대로, 한 번에 모든 연관관계에 있는 엔티티를 로딩해버리는 방식이다. @OneToMany이든, @OneToOne이든, @ManyToOne이든 연관관계에 있는 모든 엔티티를 조인 쿼리를 통해 한 번에 들고 온다.

@Entity 
public class Reply {
    //....
    
    @ManyToOne(fetch = FetchType.EAGER)
    private Board board;
    
}

언뜻 보면 굉장히 이점이 큰 것 같지만, 사실 전혀 그렇지 않다. 즉시 로딩(EAGER)은 되도록이면 지양해야 하는 방식이다. DB에 테이블 여러 개가 서로 복잡하게 연결된 경우 한 번 쿼리를 통해 엄청난 양의 엔티티를 한 번에 만들어 전달해야 할 수도 있다. 심지어 로직이 필요로 하지도 않는 엔티티마저 만들게 된다면 자원 낭비일 것이다.

실무에서는 지연 로딩(LAZY) 방식으로 기본 틀을 짠다. 하지만 조인을 처리해야 하는 상황이 많을 때 지연 로딩 방식의 단점이 부각된다. 엔티티를 한 번에 들고오지 않고 필요한 순간에만 DB 처리를 하므로 session 연결이 불필요하게 지속된다는 점이다.

이를 방지하기 위해 LAZY를 기본으로 하되, 조인이 필요한 확실한 순간에는 조인이 작동하도록 메소드를 생성하는 방식이 선호된다. 이것이 @EntityGraph이다.

@Query 자체는 매우 평범하다. @EntityGraph가 핵심이다. 이 어노테이션을 통해 같이 로딩해야 하는 다른 엔티티를 조인해서 한 번의 쿼리 실행으로 들고 올 수 있다.

 

 

2. update, insert, delete는 Cascade

@ManyToOne이든, @OneToMany든, 연관 관계가 설정된 엔티티를 가지고 save(), delete()와 같은 메소드를 사용하면 과연 이것이 연관 관계에 있는 엔티티의 테이블까지 처리할 것인가의 문제가 있다.

이것을 다루는 것이 cascade 속성이다.

@Entity
public class Reply {

    @ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.ALL})
    private Board board;
    
}

CascadeType을 ALL로 설정하면 Reply 엔티티의 Board 객체를 인식하고 이 객체의 영속성까지 한 꺼번에 처리한다.

예를 들어, Reply 엔티티의 Board 객체의 내부 상태를 변경하면 Board 테이블까지 내용이 변경(update)된다. 혹은 null로 설정하면 해당 Reply의 FK도 함께 null이 되면서 db의 테이블 상에서 관계가 깨질 것이다. 혹은 새로운 Board를 주입하면 새로운 Board도 insert되고, Reply의 FK도 변경된다.

(참고로 Board 필드를 null로 바꾼다고 삭제되는 것은 아니다. @ManyToOne에는 이것이 불가능하다. 그 이유는 맨 아래에 있다.)

@ManyToOne과는 달리, @OneToOne이나 @OneToMany 같은 관계에서는 상태를 변경했을 때, 삭제가 될 수도 있다. 즉, One에 해당하는 엔티티를 삭제하면 연결된 다른 엔티티도 모두 삭제된다. 혹은 One에 해당하는 엔티티의 내부 엔티티(Many나 One에 속하는)를 없애면 (즉 참조를 없애면) 테이블에서 삭제된다.

이때 이렇게 설정하기 위해서는 아래처럼 orphanRemoval = true로 설정해야 한다. 이것을 설정하지 않으면 디폴트인 false가 되어 참조를 변경해도 삭제까진 되진 않는다. 삭제가 되돌릴 수 없는 매우 민감한 요청이라 그렇다.

@Entity
public class Board {
	//...
    
    @OneToMany(mappedBy = "board", 
               cascade = {CascadeType.ALL},
               fetch = FetchType.LAZY,
               orphanRemoval = true)
    private Set<Reply> replies;

}

@ManyToOne에 이런 기능이 없다는 것은 생각해보면 당연하다. Many 측 엔티티에서 One에 해당하는 엔티티의 참조를 없앴다는 이유로 테이블에서 One을 삭제해버린다면, Many 측의 다른 엔티티들은 그야말로 멘붕에 빠질 것이다. 자신들이 FK로 참조하고 있던 테이블의 행이 갑작스레 사라져버렸기 때문이다!!

그러나 @OneToOne, @OneToMany는 괜찮다. One 측에서 다루기 때문에 연결된 Many나 One의 삭제를 관리해도 되기 때문이다.