OSIV 정의
Open Session In View
JPA의 영속성 컨텍스트와 하이버네이트의 session을 뷰까지 열어두는 기능
Open Session? View?
Open Session은 세션 영역을 연다는 의미인데, 정확히는 영속성 컨텍스트의 영역을 의미합니다.
View 영역은 우리가 일반적으로 생각하는 Controller 영역이나 뷰(jsp, Thymeleaf) 영역을 의미합니다.
다시 풀어보자면 Controller나 View 영역에도 영속성 컨텍스트를 유지한다는 것으로 이해할 수 있습니다.
영속성 컨텍스트의 범위는 어디까지인가?
영속성 컨텍스트는 엔티티 매니저와 연관이 깊습니다. 엔티티 매니저가 생성되는 시점에 생성되고 엔티티 매니저가 종료되는 시점에 소멸합니다. 트랜잭션 매니저를 별도로 구현하지 않고 @Transactional 어노테이션을 사용한다면 어노테이션이 붙어있는 메서드 영역 내에서 영속성 컨텍스트가 생성되고 소멸되는 것으로 이해할 수 있습니다.
@Transactional
public Member get(int id){
return memberRepository.findById(id);
}
영속성 컨텍스트의 범위를 확장해야 하는 이유는?
@Transactional 어노테이션은 보통 Service 레이어에서 많이 사용하는데 Service 레이어가 아닌 컨트롤러나 뷰 영역에서도 영속성 컨텍스트가 필요한 경우가 있을 수 있지만 많이 경험하지 못하였을 수도 있다.
😎 왜냐하면 최근에는 대부분 DTO를 사용하기 때문!
컨트롤러 영역에서는 필요한 DTO 모델을 생성하여 DAO에서 내려받은 결과 값을 DTO에 바인딩해주기 때문에 OSIV를 고려해야 하는 경우가 그리 많지 않다. 하지만 몇 년 전까지만 하더라도 JPA를 사용하면 DAO에서 내려주는 엔티티를 그대로 결과 모델로 사용하는 경우가 많았다. 그러면서 컨트롤러나 뷰 영역에서도 엔티티를 조작하는 경우가 발생했다.
기존에 @Transactional 이 붙어있는 서비스 내부에서 유지가 되던 영속성 컨텍스트의 영향 범위를 뷰 렌더링까지 유지될 수 있도록 하는 것이 필요했습니다.
OSIV의 동작원리

1. Request가 오면 Servlet Filter / Interceptor에서 Persistent Context(영속성 컨텍스트)를 생성합니다.
2. 서비스 계층에서 @Transactional로 트랜잭션을 시작할 때, 이전에 생성되어 있는 Persistent Context를 찾아서 트랜잭션을 시작합니다.
3. 메서드가 정상 종료되면 트랜잭션을 커밋하면서 종료하게 됩니다. (여기서 예외가 발생하게 되면 롤백을 진행합니다.)
4. 컨트롤러 / 뷰까지 Persistent Context가 유지되므로 조회한 엔티티는 영속 상태를 유지합니다.
5. Servlet Filter / Interceptor로 요청이 들어오면 Persistent Context를 종료합니다. 이때 flush를 호출하지 않고 바로 종료합니다.
그렇다면 뷰 영역에서 엔티티를 수정할 수도 있을까??
그렇지 않습니다!
트랜잭션을 사용하는 서비스 계층이 끝날 때 transaction이 커밋되면서 이미 flush를 했기 때문에 스프링이 제공하는 OSIV 서블릿 필터나 OSIV 스프링 인터셉터는 요청이 끝나면 flush를 호출하지 않고 em.close()로 영속성 컨텍스트만 종료해 버리므로 flush가 일어나지 않습니다. 만약 뷰 영역에서 em.flush()를 강제로 호출해도 트랜잭션 범위 밖이라는 예외가 발생하게 될 것입니다.
장점
- 지연 로딩(Lazy Loading)을 사용할 때 트랜잭션 범위를 고려하지 않고 간편하게 코드를 작성할 수 있습니다.
단점
- 컨트롤러에서 API를 호출할 때 10초가 걸리면, 10초 동안 커넥션을 반환하지 못하고 유지해야 합니다
-> 너무 오랜 시간 데이터베이스 커넥션을 사용하여, 실시간 트래픽이 중요한 서비스일 경우 커넥션이 부족해 시스템 장애로 이어질 수 있습니다.
코드 실습
OSIV true 설정
spring.jpa.open-in-view = true // OSIV가 켜져 있는 상태
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
@Transactional
public Member get(Long id){
Member member = memberRepository.findById(id).get();
return member;
}
}
@Slf4j
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
@GetMapping("/hello")
public void getName(){
Member member = memberService.get(1L);
Team team = member.getTeam();
log.info("팀 이름 : {}",team.getName()); // 뷰에서 Team 엔티티에 대한 데이터 조회 시도
}
}
2024-06-20T21:27:17.164+09:00 WARN 28848 --- [ProxyTest] [ main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
결과
2024-06-20T 21:28:05.917+09:00 INFO 28848 --- [ProxyTest] [nio-8080-exec-2] c.deepspring.proxytest.MemberController : 팀 이름 : team1
WARN은 뜨지만 결과는 잘 찍힙니다!
OSIV 해제
spring.jpa.open-in-view = false // OSIV가 꺼져 있는 상태
같은 메서드 getName()을 실행했을 때 이번에는 오류가 뜨게 됩니다.
2024-06-20T 21:29:13.541+09:00 ERROR 18200 --- [ProxyTest] [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.hibernate.LazyInitializationException: could not initialize proxy [com.deepspring.proxytest.Team#1] - no Session] with root cause org.hibernate.LazyInitializationException: could not initialize proxy [com.deepspring.proxytest.Team#1] - no Session
해결 방법 (가장 쉬운 거)
서비스 객체에서 그냥 한번 찍어주면 됩니다.
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
@Transactional
public Member get(Long id){
Member member = memberRepository.findById(id).get();
member.getTeam().getName(); // --> 여기 찍어주기
return member;
}
}
이 해결방법은 강제 실행 방법입니다.
서비스 계층의 get(Long id) 메서드에서 member.getTeam().getName()을 호출하면, 세션이 열려 있는 동안 엔티티의 연관 데이터를 강제로 로드합니다. 이때, 데이터베이스 세션은 여전히 열려 있어 Lazy Loading이 정상적으로 동작합니다.
OSIV가 비활성화된 상태에서 세션은 트랜잭션과 연관됩니다. 서비스 계층에서 트랜잭션이 열려 있는 동안 세션도 열려 있고, 엔티티와 그 연관된 엔티티를 로드할 수 있습니다. 따라서, member.getTeam().getName()을 호출함으로써 해당 연관 엔티티(Team)가 로드됩니다. 이때, 세션이 닫히기 전에 데이터를 로드하므로 이후 프레젠테이션 계층에서도 데이터를 접근할 수 있게 됩니다.
서비스 계층에서 member.getTeam().getName()을 호출하면 연관된 엔티티가 미리 로드됩니다. 이 데이터를 반환하여 컨트롤러로 전달할 때, 이미 로드된 데이터는 세션이 닫혀 있어도 사용할 수 있습니다. 결과적으로, 컨트롤러에서 해당 데이터를 접근할 때 LazyInitializationException이 발생하지 않게 됩니다.
이 문제를 다른 방법으로도 해결할 수 있습니다! (이건 다음에 기회가 된다면~😉)
- Fetch Join 사용하기
- JPA의 엔티티 그래프(Entity Graph) 사용
- DTO로 변환하여 필요한 데이터만 전달하기
- Hibernate.initialize() 사용
- 강제 로딩을 -> Proxy 객체를 사용해서 역할과 책임 분리하기
정리
영속성 컨텍스트는 트랜잭션의 생명 주기와 동일합니다.
그렇기 때문에 OSIV 옵션이 true로 설정되었다면, 영속성 컨텍스트의 생명주기를 View에 응답이 갈 때까지 유지시킵니다.
기본이 true이기 때문에 따로 설정을 하지 않았다면 Lazy 로딩을 사용했을 때
트랜잭션 범위를 벗어나고 Proxy 객체에 접근한다고 하더라도 LazyInitializationException이 발생하지 않는 것을 볼 수 있습니다.
애플리케이션에서 HTTP 요청마다 새로운 데이터베이스 커넥션이 생성되는데,
이에 따라 수많은 DB 커넥션이 발생하여 성능 저하가 발생할 수 있습니다.
데이터베이스의 커넥션은 무한하지 않고 한정적입니다.
true일 경우 API 응답이 끝날 때까지 영속성 컨텍스트와 데이터베이스 커넥션을 유지하기 때문에 요청이 빈번한 서비스에서는 이 옵션을 고려할 필요가 있습니다.
참고 자료
OSIV | Incheol's TECH BLOG
OSIV에 대해서 알아보자
incheol-jung.gitbook.io
OSIV가 왜 거기서 나와?
저번 글에서 짧게 이야기 했던 부분을 다시 이야기 해보기 위해서 먼저 코드를 보여주고, 궁금한 점을 이야기 해보겠다.UpdateMentoInfoHandler.classMentoService.class저번 글에서 가지고 있던 궁금증이 해
velog.io
[Spring/JPA] OSIV 전략이란? 언제 사용해야 할까?
OSIV (Open Session In View)OSIV란 스프링 프레임워크에서 사용하는 세션 관리 전략 중 하나로 스프링 부트에서는 기본 값이 True로 설정되어 있다.여기서 세션 (Session)은 하이버네이트의 세션. 즉, 영속
hstory0208.tistory.com