얼마 전 면접에서 "OSIV가 뭔가요?"라는 질문을 받았다. Open Session In View의 약자로 원래는 세션(영속성 컨텍스트)와 트랜잭션의 범위가 같지만, 스프링이 제공하는 OSIV를 사용하면 트랜잭션이 서비스 계층까지만 유지될 때 영속성 컨텍스트는 뷰까지 열어둘 수 있다고 했다. 추가로 트랜잭션 없이 읽기에 대해 설명하며 영속성 컨텍스트는 유지되지만 트랜잭션이 없으므로 엔티티를 조회할 수만 있고, 수정할 수는 없다고 했다. 면접에 나올 줄 전혀 몰랐던 것치고는 잘 대답한 것 같지만 확실히 공부가 필요할 것 같아 영한님 강의와 책을 찾아보았다.
- Open Session In View
- 영속성 컨텍스트를 뷰까지 열어둔다는 뜻 → 뷰에서 지연 로딩을 가능하게 해준다.
- JPA에서는 OEIV(Open EntityManager In View)라고 부른다.
Open Session In View의 핵심은 영속성 컨텍스트와 DB 커넥션이 얼마나 유지되는가이다.
기본적으로 스프링 컨테이너는 트랜잭션과 영속성 컨텍스트의 생존 범위를 동일하게 하는 전략, 즉 트랜잭션이 시작할 때 영속성 컨텍스트가 시작되고, 트랜잭션이 끝날 때 영속성 컨텍스트가 종료되는 전략을 사용한다. 또한 다양한 곳에서 엔티티 매니저를 사용하더라도 같은 트랜잭션 범위에 있다면 동일한 영속성 컨택스트를 사용한다.
문제는 프레젠테이션 계층에서 이미 준영속 상태인 엔티티를 지연 로딩하려고 할 때이다. 프록시 객체를 조회하려고 할 때 해당 객체가 초기화되어 있지 않다면 영속성 컨텍스트에 실제 엔티티 생성을 요청하고, 영속성 컨텍스트가 데이터베이스를 조회해서 엔티티 객체를 생성, 프록시 객체는 해당 엔티티 객체의 참조를 보관 후 실제 엔티티 객체의 메서드를 호출하는 일련의 과정을 거쳐야 하는데 이미 영속성 컨텍스트는 닫혀버렸기 때문에 org.hibernate.LazyInitializationException
이 발생한다.
관련해서 삽질을 상당히 많이했다 👉 org.hibernate.LazyInitializationException 에러
이럴 때 프레젠테이션 계층에서 연관된 엔티티의 지연 로딩을 가능하게 해주는 것이 바로 OSIV이다. OSIV 옵션을 켜두면 영속성 컨텍스트가 뷰까지 유지되며, 뷰에서도 지연 로딩을 사용할 수 있다.
과거에는 OSIV가 켜져있으면 클라이언트 요청이 올 때 서블릿 필터나 스프링 인터셉터에서 트랜잭션이 시작되면서 영속성 컨텍스트도 시작되고, 요청이 끝날 때 트랜잭션과 영속성 컨텍스트가 함께 종료되었다. 그러나 트랜잭션의 범위 안에 프리젠테이션 계층을 포함하는 경우, 뷰에서만 적용하고 싶은 엔티티의 변경이 데이터베이스까지 영향을 줄 수 있다. 트랜잭션의 커밋 시점이 뷰 렌더링 이후이기 때문이다.
따라서 현재 스프링 프레임워크가 제공하는 OSIV 라이브러리는 비즈니스 계층에서만 트랜잭션이 사용된다. 클라이언트로부터 요청이 들어오면 트랜잭션 시작 없이 영속성 컨텍스트만 생성되고, 서비스 계층에 있는 @Transactional
로 트랜잭션이 시작된다. 이후 트랜잭션 커밋과 영속성 컨텍스트 플러시 이후에서도 영속성 컨텍스트는 유지되기 때문에 컨트롤러와 뷰에서도 지연 로딩된 엔티티를 조회할 수 있다. 모든 작업이 끝나고 서블릿 필터나 스프링 인터셉터로 요청이 돌아오면 영속성 컨텍스트가 플러시 없이 바로 닫힌다.
참고로 트랜잭션 없이 조회하는 걸 Nontransactional reads
라고 부른다.
한 마디로 말하면 아니다. 영속성 컨텍스트를 통한 모든 변경은 트랜잭션 안에서 이루어져야 하며, 트랜잭션 밖에서 엔티티를 변경하고 영속성 컨텍스트를 플러시한다면 javax.persistence.TransactionRequiredException
이 발생한다. 따라서 기존 OSIV처럼 프레젠테이션 계층에서의 엔티티 변경을 막기 위한 여러 노력(읽기 전용 인터페이스, 엔티티 래핑) 등을 하지 않아도 된다.
※ 강제로 em.flush를 호출해도 위의 예외가 발생한다.
많은 INFO 사에에 WARN을 보면 open-in-view
옵션이 default로 enable되었다는 것을 볼 수 있다. 즉 기본값이 true로 설정되어 있기 때문에 그 동안 API 컨트롤러에서 지연로딩이 가능했던 것이다.
프레젠테이션 계층에서 엔티티 수정 직후 트랜잭션을 시작하는 서비스 계층을 호출한다면 트랜잭션 AOP가 동작하며 트랜잭션이 실행되고 해당 트랜잭션이 끝날 때 변경 감지가 동작하면서 엔티티에서 수정한 부분이 데이터베이스에 반영되어 버린다.
→ 비즈니스 로직 먼저 실행한 뒤 엔티티를 변경하면 된다.
조금 더 근본적인 문제인데, OSIV가 켜져있으면 뷰 렌더링이 이루어지거나 API가 유저에게 반환될 때까지 영속성 컨텍스트와 데이터베이스 커넥션이 유지된다. 따라서 실시간 트래픽이 중요한 어플리케이션의 경우 데이터베이스 커넥션이 빨리 반환되지 않아 부족한 문제가 생길 수 있고, 이는 중대한 장애로 이어질 수 있다. 예를 들어 컨트롤러에서 외부 API를 호출하는 경우 외부 API 대기 시간만큼 데이터베이스 커넥션도 반환되지 않는 것이다.
OSIV 옵션을 끄고 서비스에서 Command와 Query를 분리하면 된다.
이전에 이슈트레커 프로젝트를 진행할 시 IssueService 안에 너무 많은 메서드가 있어 가독성이 떨어진다는 이유로 Command와 Query를 분리하라는 리뷰를 받은 적이 있다.
그러나 당시에는 트랜잭션이나 영속성 컨텍스트 등의 개념에 대해 잘 몰랐기 때문에 OSIV 옵션은 켜둔 채로 말 그대로 한 서비스를 메서드의 역할에 따라 분리하기만 했었다.
@Transactional(readOnly = true)
를 설정하면 완벽하다. Source
몰랐는데 하나 알아가네요! 좋은 포스팅 같아요 👍