OSIV (Open Session In View)
OSIV (Open Session In View) 는 영속성 컨텍스트를 뷰까지 열어두는 기능이다. 영속성 컨텍스트가 유지되면 엔티티도 영속상태로 유지되며, 이로 인해 뷰에서도 지연 로딩을 사용할 수 있다.
JPA 에서는 OEIV(Open EntityManager In View), 하이버네이트 에서는 OSIV(Open Session In View) 라고 하지만 관례상 둘 다 OSIV 라고 부른다.
OSIV의 핵심은 영속성 컨텍스트와 DB 커넥션이 얼마나 유지되는가 이다.
기본적으로 스프링 프레임워크는 트랜잭션과 영속성 컨텍스트의 생존 범위를 동일하게 하는 전략, 즉 트랜잭션이 시작할 때 영속성 컨텍스트가 시작되고, 트랜잭션이 끝날 때 영속성 컨텍스트가 종료되는 전략을 사용한다.
하지만 이 경우에 트랜잭션이 사용되는 비즈니스 계층이 아닌 프레젠테이션 계층에서 이미 준영속 상태인 엔티티를 지연로딩 하려고 할 때 문제가 발생한다. 지연로딩을 위해 프록시 객체가 생성되고 프록시 객체를 초기화할 때 영속성 컨텍스트의 도움을 받아야 하는데, 이미 영속성 컨텍스는 닫혀버렸기 때문에 org.hibernate.LazyInitializationException이 발생한다.
이때 프레젠테이션 계층에서 연관된 엔티티의 지연로딩을 가능하게 해주는 것이 OSIV 이다.
위에서 말한 지연로딩과 프록시에 관한 내용이 궁금하다면?
지연로딩 : https://wwan13.tistory.com/20
프록시 : https://wwan13.tistory.com/21
OSIV : ON
spring.jpa.open-in-view: true
Spring Boot Jpa 의존성을 주입 받아 어플리케이션을 구성할 경우 OSIV 설정은 기본값인 True로 설정되어 있다.
트랜잭션의 범위는 Service 와 Repository 만 포함하고 있는 반면 영속성 컨텍스트는 프레젠테이션 계층까지 포함하고 있는 것을 볼 수 있다.
기본적으로 영속성 컨텍스트는 트랜잭션의 범위 안에서만 엔티티를 조회하고 수정할 수 있다. 하지만 예외적으로 엔티티를 변경하지 않고 단순 조회만 하는 동작은 트랜잭션이 없이도 동작하는데 이를 트랜잭션 없이 읽기(Nontransactional reads) 라고 한다.
따라서 OSIV 설정이 켜져있는 경우 트랜잭션의 범위에선 벗어나지만, 영속성 컨텍스트의 범위 안에 잇는 프레젠테이션 계층에서도 데이터 조회와, 프록시 초기화 즉 지연로딩이 가능하다.
JPA에서 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하려면 em.flush()를 호출해야 한다. 하지만 JPA에서 제공하는 OSIV는 요청이 끝나면 플러시를 하지 않고 em.close()로 영속성 컨텍스트만 종료시키기 떄문에 데이터베이스에 반영되지 않는다.
또, 트랜잭션 범위 밖에서 em.flush()를 강제로 호출해도 javax.persistence.TransactionRequiredException 가 발생한다.
OSIV : OFF
spring.jpa.open-in-view: off
OSIV를 끄고싶다면 resource 폴더 내부의 application.yml 에 설정을 변경 해준다.
OSIV 설정을 끄면 영속성 컨텍스트와 트랜잭션의 범위가 같아진다.
따라서 아까와 같이 프레젠테이션 계층에서 지연로딩을 시도하면 이미 영속성 컨텍스트는 닫혀버렸기 때문에 org.hibernate.LazyInitializationException가 발생한다.
이 설정을 유지하려면 엔티티 조회를 포함한 모든 로직을 트랜잭션의 범위 안으로 옮겨야 한다.
OSIV 예제
OSIV ON
@RestController
@RequireArgsConstructor
public class OrderApiController {
private final OrderRepository orderRepository;
@GetMapping("/api/orders")
public List<Order> orders() {
List<Order> all = orderRepository.findAll();
for (Order order : all) {
order.getMember().getName(); // 지연로딩
order.getDelivery().getAddress(); // 지연로딩
List<OrderItem> orderItems = order.getOrderItems();
orderItems.stream().forEach(o -> o.getItem().getName()); // 지연로딩
}
return all;
}
}
- OSIV 설정이 켜져있을때의 컨트롤러이다.
- 프레젠테이션 계층에서의 엔티티 조회와 지연로딩이 가능하기 떄문에 컨트롤러에서 지연로딩을 해주는 모습니다.
OSIV OFF
@RestController
@RequireArgsConstructor
public class OrderApiController {
private final OrderService orderService;
@GetMapping("/api/orders")
public List<Order> orders() {
return orderSercive.findAll();
}
}
@Service
@RequireArgsConstructor
@Transactional
public class OrderService {
private final OrderRepository orderRepository;
public List<Order> findAll() {
List<Order> all = orderRepository.findAll();
for (Order order : all) {
order.getMember().getName(); // 지연로딩
order.getDelivery().getAddress(); // 지연로딩
List<OrderItem> orderItems = order.getOrderItems();
orderItems.stream().forEach(o -> o.getItem().getName()); // 지연로딩
}
return all;
}
}
- OSIV가 꺼져있을 때의 모습이다.
- 컨트롤러까지 영속성컨텍스트의 범위가 미치지 못하기 때문에 엔티티 조회와 지연로딩 로직을 모두 트랜잭션 범위 안의 OrderService로 옮겨준 모습니다.
OSIV 사용시 주의점
OSIV 설정을 키고 프로젝트를 실행하면 다음과 같은 경고를 확인할 수 있다.
위 예제에서 확인했던것 처럼 OrderService를 거치지 않고 컨트롤러에서 모든 조회를 해결할 수 있는데 왜 경고를 줄까?
데이터베이스 커넥션을 유지한다는 것 자체가 이 전략의 가장 큰 장점이다. 하지만 이 전략은 너무 오랜시간동안 데이터베이스 커넥션 리소스를 사용하기 떄문에 실시간 트래픽이 중요한 애플리케이션에서는 커넥션이 모자랄 수 있다. 이는 결국 장애로 이어진다.
Command 와 Query의 분리
OSIV 설정을 킨 채로 크고 복잡한 애플리케이션을 설계하게 되면 서비스의 장애로 이어질 수 있기 때문에 실시간 트래픽이 중요한 애플리케이션 일수록 OSIV 설정을 끄는 것이 좋다.
OSIV 설정을 끄게 되면 모든 비즈니스 로직이 서비스 에 들어가게 되고, 그렇게 되면 서비스의 코드가 길어저 가독성과 유지보수성이 떨어지게 된다.
그렇게 나온 해결책이 커멘드와 쿼리의 분리 이다.
보통의 비즈니스 로직은 특정 엔티티 몇 개를 등록하거나 수정하는 것이므로 성능이 크게 문제가 되지 않는다. 하지만 문제는 조회이다. 복잡한 화면을 출력하기 위한 쿼리는 화면에 맞추어 성능을 최적화 하는 것이 중요하다.
따라서 이 둘의 관심사를 명확하게 분리하면 유지보수 관점에서 충분히 큰 의미가 있다.
OrderService (Command) : 핵심 비즈니스 로직
OrderQueryService (Query) : 화면이나 API에 맞춘 읽기 전용 서비스 (주로 읽기 전용 트랜잭션)
참고자료