영속성 컨텍스트를 뷰 단까지 열어두는 기능. 요청이 들어올 때 컨텍스트를 미리 만들고 응답이 나갈 때까지 유지한다. 서블릿의 필터나 스프링의 인터셉터가 그 일을 한다.

트랜잭션 밖의 지연 로딩

트랜잭션이 서비스에서 끝나면 컨텍스트도 사라진다. Post와 User가 다대일로 매핑된 상황에서 Post 엔티티를 컨트롤러까지 가져와 PostDto로 바꾸려 하면, 연관 객체를 건드리는 순간 could not initialize proxy 오류가 난다. 지연 로딩을 하려는데 컨트롤러에는 트랜잭션이 없고 따라서 컨텍스트도 없기 때문이다.

트랜잭션 밖에서도 영속성 컨텍스트가 살아 있어야 한다는 요구가 여기서 나왔다. OSIV가 켜져 있으면 컨트롤러에서도 지연 로딩이 되고, 서비스에서 미리 초기화하는 코드를 쓰지 않아도 된다. 스프링에서는 spring.jpa.open-in-view: true가 기본값이라 처음부터 켜져 있다. 뷰를 렌더링하다 쿼리가 나갈 수 있다는 경고가 뜨는데, 명시적으로 설정하면 없앨 수 있다.

요청부터 응답까지의 수명

동작 순서를 따라가면 트랜잭션과 컨텍스트의 수명이 어긋난다는 것이 보인다.

1. 요청 도착   필터나 인터셉터가 영속성 컨텍스트를 만든다 (트랜잭션은 아직 없다)
2. 서비스     @Transactional 이 1번의 컨텍스트를 찾아와 트랜잭션을 시작한다
3. 서비스 종료 트랜잭션을 커밋하고 플러시한다. 컨텍스트는 종료하지 않는다
4. 컨트롤러   컨텍스트가 살아 있으므로 조회한 엔티티는 영속 상태를 유지한다
5. 요청 반환   필터나 인터셉터가 플러시 없이 컨텍스트만 닫는다

쓰거나 고치는 일은 비즈니스 계층에서만 일어난다. 프레젠테이션 계층은 트랜잭션이 없어서 지연 로딩을 포함한 조회만 할 수 있다.

커넥션을 붙잡는 대가

커넥션을 응답이 나갈 때까지 붙잡는다. 가장 무거운 대가다. 컨텍스트가 살아 있다는 것은 DB 커넥션도 계속 물고 있다는 뜻이라 실시간 트래픽이 중요한 애플리케이션에서는 커넥션 풀이 마르고 장애로 이어진다. 컨트롤러에서 외부 API를 호출하면 그 대기 시간만큼 커넥션을 반환하지 못한다.

컨텍스트 공유와 넓어지는 튜닝 범위

같은 컨텍스트를 여러 트랜잭션이 공유한다. 요청 하나에 트랜잭션이 여러 번 열려도 컨텍스트는 하나다.

프레젠테이션 계층에서도 쿼리가 나가므로 성능 튜닝 범위가 컨트롤러와 뷰까지 넓어진다.

프레젠테이션 계층에서의 수정

엔티티를 고치는 일은 프레젠테이션 계층에서 일어나지 않는다. 스프링 OSIV는 요청이 끝날 때 플러시를 호출하지 않고 em.close()로 컨텍스트만 닫기 때문이다. 강제로 em.flush()를 불러도 트랜잭션 범위 밖이라 TransactionRequiredException이 난다. 다만 컨트롤러가 엔티티를 고친 뒤 트랜잭션이 있는 비즈니스 로직을 다시 호출하면, 컨텍스트를 공유하고 있으므로 그 수정이 반영된다. 보통은 비즈니스 로직을 먼저 부르고 그 결과를 조회하므로 이 경우는 잘 생기지 않는다.

켜고 끄는 판단

켜고 끄는 판단은 커넥션을 오래 쥐는 것이 감당 가능한지에 달려 있다. 실시간 트래픽이 중요한 서비스라면 끄는 쪽을 택한다. 대신 모든 지연 로딩을 트랜잭션 안에서 처리해야 하므로, 서비스 계층에서 fetch join이나 Batch Size로 필요한 것을 미리 가져오거나 DTO로 변환해 내보낸다.

관리자 페이지처럼 트래픽이 적은 내부 서비스라면 켜둬도 큰 문제가 없다. 소규모 서비스는 보통 켜둔 채로 간다.

관련

출처