[JPA] OSIV(Open Session In View)

janeljs·2021년 10월 14일
26
post-thumbnail

얼마 전 면접에서 "OSIV가 뭔가요?"라는 질문을 받았다. Open Session In View의 약자로 원래는 세션(영속성 컨텍스트)와 트랜잭션의 범위가 같지만, 스프링이 제공하는 OSIV를 사용하면 트랜잭션이 서비스 계층까지만 유지될 때 영속성 컨텍스트는 뷰까지 열어둘 수 있다고 했다. 추가로 트랜잭션 없이 읽기에 대해 설명하며 영속성 컨텍스트는 유지되지만 트랜잭션이 없으므로 엔티티를 조회할 수만 있고, 수정할 수는 없다고 했다. 면접에 나올 줄 전혀 몰랐던 것치고는 잘 대답한 것 같지만 확실히 공부가 필요할 것 같아 영한님 강의와 책을 찾아보았다.

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가 켜져있으면 클라이언트 요청이 올 때 서블릿 필터나 스프링 인터셉터에서 트랜잭션이 시작되면서 영속성 컨텍스트도 시작되고, 요청이 끝날 때 트랜잭션과 영속성 컨텍스트가 함께 종료되었다. 그러나 트랜잭션의 범위 안에 프리젠테이션 계층을 포함하는 경우, 뷰에서만 적용하고 싶은 엔티티의 변경이 데이터베이스까지 영향을 줄 수 있다. 트랜잭션의 커밋 시점이 뷰 렌더링 이후이기 때문이다.

스프링 OSIV: 비즈니스 계층 트랜잭션

따라서 현재 스프링 프레임워크가 제공하는 OSIV 라이브러리는 비즈니스 계층에서만 트랜잭션이 사용된다. 클라이언트로부터 요청이 들어오면 트랜잭션 시작 없이 영속성 컨텍스트만 생성되고, 서비스 계층에 있는 @Transactional로 트랜잭션이 시작된다. 이후 트랜잭션 커밋과 영속성 컨텍스트 플러시 이후에서도 영속성 컨텍스트는 유지되기 때문에 컨트롤러와 뷰에서도 지연 로딩된 엔티티를 조회할 수 있다. 모든 작업이 끝나고 서블릿 필터나 스프링 인터셉터로 요청이 돌아오면 영속성 컨텍스트가 플러시 없이 바로 닫힌다.
참고로 트랜잭션 없이 조회하는 걸 Nontransactional reads라고 부른다.

영속성 컨텍스트가 열려있다면 데이터의 수정도 가능한 걸까?

한 마디로 말하면 아니다. 영속성 컨텍스트를 통한 모든 변경은 트랜잭션 안에서 이루어져야 하며, 트랜잭션 밖에서 엔티티를 변경하고 영속성 컨텍스트를 플러시한다면 javax.persistence.TransactionRequiredException이 발생한다. 따라서 기존 OSIV처럼 프레젠테이션 계층에서의 엔티티 변경을 막기 위한 여러 노력(읽기 전용 인터페이스, 엔티티 래핑) 등을 하지 않아도 된다.
※ 강제로 em.flush를 호출해도 위의 예외가 발생한다.

난 OSIV 모르는데도 지금까지 엔티티 조회 잘 해왔는데?


많은 INFO 사에에 WARN을 보면 open-in-view 옵션이 default로 enable되었다는 것을 볼 수 있다. 즉 기본값이 true로 설정되어 있기 때문에 그 동안 API 컨트롤러에서 지연로딩이 가능했던 것이다.

OSIV 사용 시 주의사항

  1. 프레젠테이션 계층에서 엔티티 수정 직후 트랜잭션을 시작하는 서비스 계층을 호출한다면 트랜잭션 AOP가 동작하며 트랜잭션이 실행되고 해당 트랜잭션이 끝날 때 변경 감지가 동작하면서 엔티티에서 수정한 부분이 데이터베이스에 반영되어 버린다.
    → 비즈니스 로직 먼저 실행한 뒤 엔티티를 변경하면 된다.

  2. 조금 더 근본적인 문제인데, OSIV가 켜져있으면 뷰 렌더링이 이루어지거나 API가 유저에게 반환될 때까지 영속성 컨텍스트와 데이터베이스 커넥션이 유지된다. 따라서 실시간 트래픽이 중요한 어플리케이션의 경우 데이터베이스 커넥션이 빨리 반환되지 않아 부족한 문제가 생길 수 있고, 이는 중대한 장애로 이어질 수 있다. 예를 들어 컨트롤러에서 외부 API를 호출하는 경우 외부 API 대기 시간만큼 데이터베이스 커넥션도 반환되지 않는 것이다.

그럼 어떻게 해?

OSIV 옵션을 끄고 서비스에서 Command와 Query를 분리하면 된다.
이전에 이슈트레커 프로젝트를 진행할 시 IssueService 안에 너무 많은 메서드가 있어 가독성이 떨어진다는 이유로 Command와 Query를 분리하라는 리뷰를 받은 적이 있다.

그러나 당시에는 트랜잭션이나 영속성 컨텍스트 등의 개념에 대해 잘 몰랐기 때문에 OSIV 옵션은 켜둔 채로 말 그대로 한 서비스를 메서드의 역할에 따라 분리하기만 했었다.

  1. OSIV를 false로 설정해주고
  2. 핵심 비즈니스 로직에 관련된 메서드는 IssueCommandService에, 화면이나 API에 맞춘 서비스는 IssueQueryService에 넣어주자.
  3. 마지막으로 QueryService에@Transactional(readOnly = true)를 설정하면 완벽하다.

Source

9개의 댓글

comment-user-thumbnail
2021년 10월 17일

몰랐는데 하나 알아가네요! 좋은 포스팅 같아요 👍

1개의 답글
comment-user-thumbnail
2021년 10월 18일

넘모 어렵지만 재미있는 자바 월드 🤟

1개의 답글
comment-user-thumbnail
2021년 10월 19일

d

1개의 답글
comment-user-thumbnail
2021년 10월 21일

와 가끔 트랜잭션 예외가 났는데 이런게 있었군요 감사합니다

1개의 답글