[JPA] could not initialize proxy - no Session org.hibernate.LazyInitializationException

2022. 9. 7. 03:07JPA 기초

서비스 계층에서 아래의 modify 메소드를 작성하였고, 두 번째 이미지의 테스트 코드를 작성했다.
modify 메소드는, PK로 Board를 가져온 뒤 내용과 제목을 변경한 후 save하는 메소드다.
서비스 계층에는 트랜잭션이 없으므로, 변경감지를 사용하지 않고 save 메소드를 통해 엔티티를 변경하였다.

BoardService를 주입받은 뒤 아래의 modifyTest() 메소드를 실행한 결과 

실행한 테스트 코드

could not initialize proxy [com.example.board.entity.Board#2] - no Session
org.hibernate.LazyInitializationException: could not initialize proxy [com.example.board.entity.Board#2] - no Session

이런 에러 로그를 남기고 테스트는 실패했는데, JPA에서 지연 로딩을 사용한 경우 몇 번 맞닥뜨리게 되는 에러다. 

발생 원인

서비스 계층에서 사용한 아래의 메소드는

boardRepository.getOne(dto.getBno());

기본적으로 지연 로딩방식을 사용한다. 즉, 프록시 객체를 반환한다.

그리고, getOne()메소드는 기본적으로 deprecated 되었다. 

위의 스프링 레퍼런스에서 대신 사용하라고 알려준 메소드 getReferenceById(id)를 보면

내부에서 EntityManager.getReference를 호출하여 프록시 객체를 호출한다. 
getOne()메소드도 마찬가지로 프록시를 반환한다.

다시 돌아가서, Service 코드를 자세히 살펴보면,

    public void modify(BoardDto dto) {
        log.info("dto={}", dto);
        Board board = boardRepository.getOne(dto.getBno());
		//지연 로딩상태
        
        board.changeContent(dto.getContent());
        //changeContent를 만난 순간 board를 select하는 쿼리가 발생한다.
        board.changeTitle(dto.getTitle());

        boardRepository.save(board);
    }

주석에 달아 놓았듯, getOne()메소드는 프록시를 반환하므로, board가 사용될 때 영속성 컨텍스트는 1차 캐시에서 조회한 뒤, 없다면 쿼리를 DB에 전송하여 board를 가져온다.

이후 board의 내용과 제목을 바꾼 뒤 저장되어야 하는데
제목처럼 LazyInitializationException가 발생하는 이유는 트랜잭션이 없기 때문이다. 

영속성 컨텍스트는 트랜잭션과 생명주기를 같이한다. 현재 서비스 계층에는 트랜잭션이 없다.
Spring Data Jpa는 기본적으로 SimpleJpaRepository의 메소드를 이용하는데  SimpleJpaRepository의 메소드에는 이미 트랜잭션이 걸려있다. 

따라서

Board board = boardRepository.getOne(dto.getBno());

이 코드를 통해 Board를 반환받은 순간 트랜잭션은 끝나고 영속성 컨텍스트는 닫히게 된다. 즉, board로 참조하고 있는 엔티티는 준영속 상태가 되는 것이다.

board.changeContent(dto.getContent());

이 코드를 실행할때면 이미 트랜잭션 X, 영속성 컨텍스트 X인 상태다. 이제 no session에러가 발생하는 이유가  납득이 된다.

해결 방안

1. 테스트 메소드나, 서비스 메소드(혹은 클래스)에 @Transactional 어노테이션을 추가한다. 

서비스 계층에 @Transactional로 트랜잭션을 시작하면, SimpleJpaRepository에 존재하는 @Transactional은 propagation이 모두 다 기본값인 REQUIRED로, 서비스 계층에서 시작한 트랜잭션에 합쳐진다.

아무튼 그래서 Board 엔티티의 내용과 제목을 변경할때까지 트랜잭션과 영속성 컨텍스트가 열려있으므로 해결이 가능하다.

2. getOne(), getById() 메소드 대신에 findById() 메소드를 사용한다.

findById() 메소드는 기능은 동일하나, 프록시 객체가 아닌 실제 객체를 반환한다.
따라서 서비스 계층에 트랜잭션이 없어도 정상적으로 내용과 제목이 변경된다.

3. OSIV(Open Session In View) 설정

영속성 컨텍스트는 트랜잭션과 생명주기를 같이하나, OSIV를 활성화하면 @Transactional 메소드가 종료된 이후에도 컨텍스트가 유지된다. 
MVC 패턴에서는 view 페이지를 렌더링해서 반환할 때까지 기다렸다가 Connection을 반환하면서 영속성 컨텍스트도 닫힌다. 다만 Connection을 오랫동안 물고 있으므로 주의해서 사용해야 한다.