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

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

by exdus3156 2024. 1. 24.

1. 지연 로딩이란?

아래와 같은 아주 간단한 Reply(댓글) Entity 클래스가 있다.

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

댓글(Reply) 엔티티는 Board(게시판) 엔티티와 연결되어 있고, 그 관계는 댓글이 Many, 게시판이 One 관계다. 즉, 게시판 하나에 여러 개의 댓글이 달린 형태이며, 데이터베이스에서는 댓글(Reply) 테이블에 FK가 설정될 것이다.

만약 ReplyRepository 영속성 객체에서 댓글 하나를 가져온다고 하자.

public void getReply() {
    Option<Reply> option = replyRepository.findById(100L);
    Reply reply = option.orElseThrow();
}

이때, Reply 엔티티 내부의 Board는 과연 로딩이 될까?

바로 이 부분을 다루는 것이 FetchType이다. 

상식적으로 생각하면 조인 쿼리를 통해 Reply와 연결된 Board까지 한번에 가져와야 할 것 같다. 하지만 이것은 성능적으로 그리 좋지 않은 방식이다.

왜냐하면 만약 Board 내부에 다시 어떤 엔티티가 있다고 하자. 예를 들어, Board(게시판)에 달린 첨부 이미지(Images)들이다. Reply 하나를 가져올 때 Board까지 가져와야 한다면 Board 내부의 Image 엔티티까지 전부 가져와야 한다. 만약 이 Image 엔티티 내부에도 또 다른 관계의 엔티티가 있다면 연쇄 반응이 일어나 성능 저하가 발생할 것이다.

그래서 일반적으로 영속성 객체에서 엔티티를 가져올 때는 FetchType을 LAZY로 설정하여 로딩을 지연한다. 지연한다는 것의 의미는 Reply(댓글) 엔티티를 DB에서 가져오되, 내부의 Board(게시판)까지는 로딩하지 않겠다는 것이다.

물론 아예 하지 않는다는 것은 아니다. LAZY(지연 로딩)이란, 만약 가져온 Reply 엔티티에서 getBoard()와 같은 메소드를 통해 내부의 Board 객체를 필요로 하는 순간, 바로 그때 다시 영속 객체에서 Board 객체를 가져와 로딩한다.

물론 이 작업을 위해서는 @Transactional을 통해 스프링 JPA가 session을 바로 종료하지 않도록 설정해줄 필요는 있다. session을 유지하지 않으면 reply.getBoard()를 하는 시점에 이미 session이 종료되기 때문에 JPA가 DB 쿼리를 실행할 수가 없다.

 

 

2. 영속성 컨텍스트와 @Transactional

@Transactioal
public get Reply() {
    Optional<Reply> option = replyRepository.findById(100L);
    Reply reply = option.orElseThrow();
    
    // ...
    
    Board board = reply.getBoard();
    
    //...
}

코드를 보면 꽤 신기하게 느껴지는 지연 로딩이다. 왜냐하면 이미 replyRepository에서 Reply 엔티티를 가져왔기 때문이다. 이미 가져오고 난 후에 getBoard()를 하는데, 그것이 다시 영속 처리가 된다는 것은 신기하다. JpaRepository에서 메소드를 실행하는 것도 아닌데 말이다.

이미 가져오고 나서도 필요한 엔티티 호출 시 다시 DB를 수행할 수 있는 이유는 내부적으로 JpaRepositorty가 영속성 컨텍스트로 엔티티를 관리하기 때문이다.

JpaRepository는 자신이 가져온 엔티티의 생명주기를 관리한다. 즉, 엔티티를 DB에서 가져오고 땡 하고 끝나는 것이 아니다.

EntityManager를 통해 영속성 컨텍스트를 관리할 수 있다. JpaRepository는 내부적으로 이렇게 엔티티를 추적하고 관리한다. 이에 따라 지연 로딩이 가능한 것이며, 더 나아가 @Transactional을 통해 생성된 트랜잭션 범위 안에서 session 연결을 유지하며 필요한 DB 처리가 식별되면 처리해주는 것이다.

물론 지연 로딩을 위해 session 연결을 유지하는데, 이를 위해 열어둔 트랜잭션을 닫고 최종 커밋을 수행할 수 있도록 @Commit을 설정해야 한다.

 

 

3. @ToString에 exclude를 적는 이유

@Entity
@ToString(exclude = "board")
public class Reply {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long rno;
    
    @ManyToOne(fetch = FetchType.LAZY)
    private Board board;

}

 

내부에 연관 관계가 있는 경우, FetchType을 LAZY로 설정하는 것은 위에서 설명했다시피, 조인(join) 쿼리로 모든 데이터를 한 꺼번에 로딩하는 것을 방지하기 위해서다. 대신 해당 엔티티가 필요한 순간에만 로딩을 한다.

따라서 롬복 사용 시, @ToString에 exclude 값을 통해 내부 엔티티를 출력하지 않겠다고 명시해야 한다. 

왜냐하면 개발을 할 때는 내부 값을 보기 위해 콘솔에 출력을 자주 하게 되는데, 출력을 하기 위해 내부의 엔티티 값에 접근하면 그 즉시 DB 처리가 수행되기 때문이다.

기껏 FetchType을 LAZY로 설정해 로딩을 지연한다고 해도 ToString을 사용할 때마다 내부 값에 접근하는 코드가 수행되므로 지연되지 않고 무조건 내부 엔티티까지 로딩되는 것이다.

콘솔 출력 시에는 로딩을 방지하기 위해 exclude로 제외하는 것이 좋은 이유다.