-
OSIV와 성능 최적화JPA/JPA 활용 2023. 8. 11. 14:02
OSIV (Open Session In View)
스프링부트에서 트랜잭션을 시작할 때 데이터베이스 커넥션을 가지고온다.
-> 데이터베이스 커넥션 : 한 번에 접근할 수 있는 쓰레드 풀 같은 커넥션
이 때 OSIV가 켜져있으면 트랜잭션이 끝나고 컨트롤러 계층까지 가도 데이터베이스 커넥션을 반환하지 않는다. 그 이유는 Lazy 로딩을 생각하면 데이터가 필요한 시점에 프록시 객체를 db에서 가져오기 때문이다. 영속성 컨텍스트가 데이터베이스를 계속 붙잡고 있어야 한다. api가 유저한테 반환이 되고 response가 나갈 때 까지 데이터베이스 커넥션을 붙잡고 있는 것이다.
OSIV는 기본이 true, 켜져있는 상태이다.
OSIV 장점
지연 로딩은 영속성 컨텍스트가 살아있어야 하고, 영속성 컨텍스트는 기본적으로 데이터베이스 커넥션을 유지한다는 큰 장점이 있다.
OSIV 단점
하지만 이 전략은 치명적인 단점이 있는데, 너무 오랜시간동안 데이터베이스 커넥션 리소스를 사용하기 때문에 실시간 트래픽이 매우 중요한 애플리케이션에서는 커넥션이 모자랄 수 있어서 최악의 경우에는 장애까지 이어질 수 있다.
예를 들어 컨트롤러 계층에서 또 다른 외부의 API를 호출하면 외부 API 대기 시간까지 커넥션을 반환받지 못하여 다른 유저들이 접근할 때 장애가 일어난다.
OSIV OFF - spring.jpa.open-in-view: false
OSIV를 끄면 트랜잭션을 종료할 때 영속성 컨텍스트를 닫고 데이터 커넥션을 반환한다. 그래서 짧은 시간 내에 데이터 커넥션을 반환한다. 서비스 계층에서 join이라는 멤버를 저장하는 로직이 있다고 하면 그 시점에만 데이터 커넥션을 사용하고 컨트롤러에서 호출을 할 시에는 데이터 커넥션과 영속성 컨텍스트 둘 다를 사용하지 않는다.
사용자 요청이 많을 시에는 커넥션을 유용하게 사용할 수 있다는 장점이 있다.
하지만 지연로딩을 트랜잭션 안에서 처리해야한다는 단점이 있다. 지금까지 많은 지연 로딩 코드들을 작성했는데 이러한 코드들을 전부 트랜잭션 안으로 넣어야하는 것이 큰 단점이다.
OSIV를 꺼둔 상황에서 컨트롤러 계층에서 지연로딩을 사용하면 에러가 발생하게 된다. could not initialize proxy라고 해서 프록시를 초기화하지 못한다는 에러가 발생한다.
해결 방법
1. OSIV를 켜놓음
2. fetch join을 사용
3. 지연 로딩 코드를 트랜잭션 안으로 넣기
3번에 대해서 - QueryService라는 계층을 따로 만든다.
Service 계층에 Lazy로딩을 위한 QueryService를 만든 다음에 컨트롤러의 로직들을 다 넘긴다. 이후에 컨트롤러에서는 서비스에서 코드를 반환만 하면 된다.
@GetMapping("/api/v3/orders") public List<OrderDto> ordersV3() { List<Order> orders = orderRepository.findAllWithItem(); List<OrderDto> collect = orders.stream() .map(o -> new OrderDto(o)) .collect(Collectors.toList()); return collect; }
원래는 컨트롤러에서 루프를 돌려서 값을 찾기 때문에 Lazy 로딩이 발생한다.
Service 계층에 OrderQueryService를 만들어서 로직들을 넘긴다.
@Service @RequiredArgsConstructor @Transactional public class OrderQueryService { private final OrderRepository orderRepository; public List<OrderDto> ordersV3() { List<Order> orders = orderRepository.findAllWithItem(); List<OrderDto> collect = orders.stream() .map(o -> new OrderDto(o)) .collect(toList()); return collect; } }
@GetMapping("/api/v3/orders") public List<jpabook.jpashop.service.query.OrderDto> ordersV3() { return orderQueryService.ordersV3(); }
컨트롤러 계층에서는 QueryService를 호출해서 반환해주는 역할만 한다.
이렇게 설정하면 OSIV를 끄더라도 Lazy 로딩 exception이 발생하지 않는다.
보통 비즈니스 로직은 특정 엔티티 몇 개를 등록하거나 수정하는 것이므로 성능이 크게 문제가 되지 않는다. 그런데 복잡한 화면을 출력하기 위한 쿼리는 화면에 맞춰서 성능을 최적화 하는 것이 중요하다.
크고 복잡한 애플리케이션을 개발한다면, 이 둘의 관심사를 명확하게 분리하는 선택은 유지보수 관점에서 충분히 의미가 있다.
OrderService 결론
- OrderService : 핵심 비즈니스 로직
- OrderQueryService : 화면이나 API에 맞춘 서비스 (주로 읽기 전용 트랜잭션)
trade-off가 있기 때문에 애플리케이션이 작으면 굳이 이런 방식을 하지 않아도 되고 애플리케이션이 너무 커지면 OSIV를 끄는 것이 좋다고 한다.
고객 서비스를 위한 실시간 API는 OSIV를 끄고, ADMIN 처럼 커넥션을 많이 사용하지 않는 곳에서는 OSIV를 켠다.
'JPA > JPA 활용' 카테고리의 다른 글
컬렉션 조회 최적화 3 - 페이징과 한계점 돌파 (0) 2023.08.10 컬렉션 조회 최적화 2 - fetch join으로 최적화 (0) 2023.08.10 컬렉션 조회 최적화 1 - 페치 조인을 안 했을 시 (0) 2023.08.10 간단한 주문조회 API - 지연 로딩과 조회 성능 최적화(2) (0) 2023.07.11 간단한 주문조회 API - 지연 로딩과 조회 성능 최적화 (0) 2023.07.11