Spring Boot로 JPA를 쓰다 보면 OSIV(Open Session in View) 설정을 켠 채로 개발하는 경우가 많다.
심지어 해당 설정의 존재를 모르면 그냥 써야한다. 이유는 단순하다. 기본값이 켜져 있기 때문이다.
Spring Boot는 웹 애플리케이션에서 기본적으로 OpenEntityManagerInViewInterceptor를 등록하고, spring.jpa.open-in-view의 기본값도 true다.
그런데 REST API 위주로, 서비스 계층에서 DTO로 변환해서 반환하는 구조를 쓰고 있다면 이런 의문이 생긴다.
“컨트롤러에서 엔티티를 직접 안 쓰는데, OSIV를 굳이 켜둘 이유가 있나?”
이 글은 그 질문에 답하기 위한 글이다.
OSIV가 무엇인지, 왜 만들어졌는지, 내부적으로 EntityManager와 영속성 컨텍스트를 어떻게 유지하는지, 그리고 DTO 중심 REST 구조에서는 왜 필요성이 낮아지는지까지 정리해본다.
OSIV는 이름 그대로 웹 요청이 끝날 때까지 EntityManager를 열어두는 패턴이다.
Spring Framework의 OpenEntityManagerInViewInterceptor와 OpenEntityManagerInViewFilter는 둘 다 요청 전체 처리 동안 EntityManager를 현재 스레드에 바인딩한다.
그리고 이 패턴의 목적은 원래 트랜잭션이 끝난 뒤에도 웹 뷰에서 lazy loading을 가능하게 하기 위해서다.
즉, OSIV의 본질은 이것이다.
트랜잭션이 끝났더라도 요청이 아직 살아 있는 동안에는 같은 EntityManager를 통해 LAZY 연관관계 접근을 허용한다
이 기능은 서버 사이드 렌더링 템플릿, JSP, Thymeleaf 같은 환경에서는 편리하다. 서비스 계층에서 엔티티만 넘겨도, 뷰를 그리는 도중 연관 엔티티를 추가로 읽을 수 있기 때문이다.
OSIV는 “좋은 설계”를 위해 생긴 기능이라기보다, 웹 뷰 렌더링의 편의성을 위해 생긴 기능에 가깝다. 트랜잭션은 이미 끝났는데 뷰에서 order.member.name 같은 식으로 연관 객체를 읽고 싶을 때, 보통은 LazyInitializationException이 난다. OSIV는 그 시점까지 EntityManager를 열어두어 이 문제를 피하게 해준다.
정리하면 OSIV는 이런 문제를 해결하려고 등장했다.
서비스 계층에서 필요한 데이터를 다 준비하지 못해도 뷰 계층에서 연관 엔티티를 읽을 수 있게 하고 트랜잭션 종료 이후의 지연 로딩을 허용한다
OSIV가 켜져 있으면 Spring은 요청이 시작될 때 EntityManager를 열고, 그걸 현재 요청 스레드에 바인딩한다. 해당 EntityManager가 현재 스레드에서 사용할 수 있게 되며, 트랜잭션 매니저가 이를 자동으로 감지한다. 또한 이 방식은 non-transactional read-only execution에도 적합하다.
여기서 중요한 포인트는 하나다.
OSIV는 트랜잭션을 길게 잡는 기능이 아니라, EntityManager의 생명주기를 요청 단위로 늘리는 기능이다.
각 EntityManager 인스턴스는 하나의 고유한 영속성 컨텍스트와 연결된다. 그리고 그 영속성 컨텍스트 안에서는 같은 엔티티 식별자에 대해 하나의 인스턴스만 존재한다. (동일성 보장) 또한 이 컨텍스트에 붙어 있는 엔티티는 managed 상태이고, 변경 감지가 일어나는 대상이 된다.
즉, EntityManager가 살아 있는 동안 그 안의 영속성 컨텍스트도 살아 있고 그 안에 있는 엔티티들이 1차 캐시처럼 동작한다
따라서 OSIV가 켜져 있는 웹 요청에서는 EntityManager가 요청 끝까지 유지되므로, 그에 연결된 영속성 컨텍스트와 1차 캐시도 사실상 요청 단위로 유지된다고 볼 수 있다.
JpaTransactionManager는 JPA용 트랜잭션 매니저이고, EntityManagerFactory에서 가져온 EntityManager를 현재 스레드에 바인딩한다.
그리고 Spring의 SharedEntityManagerCreator는 현재 트랜잭션용 EntityManager가 있으면 그쪽으로 위임하고, 없으면 호출 단위로 새 EntityManager를 만든다.
이걸 일반적인 웹 요청 흐름에 대입하면 보통 이렇게 이해할 수 있다.
EntityManager를 요청 스레드에 바인딩한다@Transactional 진입JpaTransactionManager가 현재 스레드의 EntityManager를 트랜잭션에 참여시킨다EntityManager는 계속 살아 있을 수 있다EntityManager와 영속성 컨텍스트가 정리된다즉, OSIV는 요청 전체에 EntityManager를 열어두고, @Transactional은 그중 일부 구간에만 트랜잭션을 건다고 이해하면 가장 정확하다.
그렇다. OSIV가 켜져 있고 요청이 아직 끝나지 않았다면, 트랜잭션은 끝났어도 아직 초기화되지 않은 LAZY 연관관계를 접근할 때 SQL이 나갈 수 있다. OSIV를 “원래 트랜잭션이 끝난 뒤에도 lazy loading을 허용하기 위한 패턴”이라고 설명하는 이유가 바로 이것이다.
예를 들어 서비스 계층에서 Order만 조회하고 트랜잭션이 끝난 뒤, 컨트롤러나 직렬화 과정에서 order.getMember().getName()을 처음 건드리면 추가 SELECT가 발생할 수 있다.
transaction-scoped persistence context에서 트랜잭션 없이 persist, remove, merge, refresh를 호출하면 TransactionRequiredException이 발생해야 한다고 규정한다. 반면 일반 조회와 이미 관리 중인 엔티티의 상태 확인, LAZY 로딩 같은 읽기 성격의 동작은 가능할 수 있다.
persist, remove, merge, refresh는 트랜잭션 없이는 안 된다고 보는 게 맞다이 부분이 나에게 제일 큰 의문이였다.
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
@GetMapping("/get/{id}")
public ResponseEntity<OrderResponse> getOrder(@PathVariable Long id) {
return ResponseEntity.ok(orderService.getOrder(id));
}
}
난 위와 같이 REST API 위주로 컨트롤러를 구현하고 뷰를 사용하지 않는다. 그렇기 때문에 전체적인 설계가 DB에 액세스 하는 경우를 제외하고는 엔티티를 호출할 일이 없고 DTO를 통해 데이터를 다루는데 굳이 이런 기능이 필요할까.
이 구조의 핵심은 명확하다.
이 구조라면 OSIV의 본래 목적, 즉 트랜잭션이 끝난 뒤 웹 뷰나 응답 생성 단계에서 지연 로딩을 허용하는 기능이 거의 필요 없다. Spring Boot 공식 문서도 OSIV를 “웹 뷰에서 lazy loading을 허용하기 위한 것”이라고 설명하고 있으므로, 컨트롤러가 엔티티를 직접 쓰지 않고 서비스에서 DTO로 끝내는 구조에서는 OSIV의 장점이 크게 줄어든다고 보는 게 자연스럽다.
즉,
“컨트롤러가 엔티티를 안 만지고, 서비스 안에서 DTO 변환을 끝내는데, 요청 끝까지
EntityManager를 굳이 열어둘 이유가 있나?”
내 답은 이렇다.
대부분의 순수 REST API에서는 굳이 그럴 이유가 크지 않은 것 같다.
즉, 결론은 “OSIV는 무조건 끄자”가 아니다.
OSIV는 서버 사이드 뷰에 맞는 도구이고, DTO 중심 REST API에는 필요성이 낮은 도구라고 보는 쪽이 더 정확하다.