스프링/JPA

[JPA 기초] OSIV (Transaction 생존 범위에 따른 성능, 대처에 대해)

nomoreFt 2022. 4. 5. 16:31

OSIV

Open Session in view : 하이버네이트
Open EntityManager in view : JPA
관례상 OSIV라고 한다. (JPA 가 나중에 나옴)

서버시작때 warn을 주는 모습

image
    2022-02-14 00:12:00.577  WARN 19764 --- [  restartedMain] 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   

OSIV 기본값은 true인데, true로 되어있으면 영속성 컨텍스트가 Transaction (Service -> Repository) 끝나도, Controller, View, Response 끝날 때 까지
영속성 Context란 DB Connection이 끝까지 살아있다. (View Render, Data Response Return 되면 사라진다.)
그렇기 때문에 API, View Template에서 지연 로딩사용이 가능했다.
지연로딩은 영속성 Contenxt가 살아있어야 가능하고, 영속성 컨텍스트는 기본적인 데이터베이스 커넥션을 유지한다.

OSIV 치명적인 단점!

너무 오랜 시간동안 DB Connection 리소스를 사용하기 때문에, 실시간 트래픽이 중요한 어플리케이션에서는 Connection이 모자랄 수 있다. 이건 결국 장애로 이어짐.
보통은 Service -> Repository에서 DB 값 가져오고 끝나는데, OSVI는 true이면 계속 클라이언트에게 Response 갈 때까지 물고있기 때문에
ex) 컨트롤러에서 외부 API를 호출하면, 외부 API 대기시간만큼 커넥션 리소스를 반환하지 못하고 대기해야한다.

영속성 컨텍스트 생존 범위, 수정 범위

image

OSIV false시에

image

Service, Repository범위에서만 영속성 Context 유지, DB Connection 유지

Service -> Repository에서 영속성 컨텍스트를 받고, DB Connection도 반환한다. :ㅣ Connection 리소스를 낭비하지 않는다.

OSIV false 단점

OSIV를 끄면, 모든 지연로딩을 트랜잭션 안에서만 처리해아한다. 따라서 지금까지 작성한 많은 코드가 Controller에서 처리한 경우가 많은데, 모두 트랜잭션 안에
밀어넣어야 한다. 그리고 viewTemplate에서 LAZY LOADING이 지원되지 않는다.

OSIV true, false Test

image

Controller 시점에서 LAZY LOADING 호출하는 v1 API

  @GetMapping("/api/v1/orders")
    public List<Order> ordersV1() {
        List<Order> all = orderRepository.findAll(new OrderSearch());
        //한번씩 호출하면서 LAZY LOADING 호출
        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 true 시에는 잘나오는데, false 시에는 Proxy 에러가 난다.

image

OSIV false라서 Controller에는 Transaction이 이미 반환되어서 DB Connection, 영속성 Context가 반환되어있기 때문.

Connection 부족 문제를 해결하기 위해 OSIV false로 뒀을 때, 해결방안 -> @Transaction을 붙인 Service를 새로 파서 거기서 LAZYLOADING 작업 수행 뒤, Controller 반환

기존 Controller에 있는 메서드, OSIV true 로써, Controller에서 LAZY LAODING을 이용해 OrderDto로 변환하는 모습이다.

   public List<OrderDto> orderV3(){
        List<Order> all = orderRepository.findAllWithItem(new OrderSearch());
        return all.stream().map(o -> new OrderDto(o)).collect(toList());
    }      
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class OrderQueryService {

    private final OrderRepository orderRepository;

    public List<OrderDto> orderV3(){
        List<Order> all = orderRepository.findAllWithItem(new OrderSearch());
        return all.stream().map(o -> new OrderDto(o)).collect(toList());
    }

    @Data
    static class OrderDto {

        private Long orderId;
        private String name;
        private LocalDateTime orderDate;
        private OrderStatus orderStatus;
        private Address address;
        //OrderItem도 Entity기 때문에 바로 반환하면 안된다. OrderItem도 DTO로 모두 변환해야함!!!
        private List<OrderApiController.OrderItemDto> orderItems;

        public OrderDto(Order order) {
            this.orderId = order.getId();
            this.name = order.getMember().getName();
            this.orderDate = order.getOrderDateTime();
            this.orderStatus = order.getStatus();
            this.address = order.getDelivery().getAddress();
            this.orderItems = order.getOrderItems().stream()
                    .map(o -> new OrderApiController.OrderItemDto(o))
                    .collect(toList());
        }
    }

필수 로직이 새로 생성된 @Transaction 범위 안의 Service로 빠진 Controller 메서드 모습.

   @GetMapping("/api/v3/ordersNotDistinct")
    public List<OrderDto> orderV3(){
        return orderQueryService.orderV3();
    }      

쿼리용 전용 Service를 만들고, Transaction을 처리하는 로직 삽입한다.

OSIV false시에 해야할 일

->OrderService : 핵심 비즈니스 로직
->OrderQueryService : 화면이나 API에 맞춘 서비스 (주로 읽기 전용 트랜잭션 사용)
이런 식으로 관심사를 명확하게 분리하는 선택을 택한다.

결론

  1. OSIV true시에 Connection은 부족하지만, OrderQueryService같은것을 생성하지 않아도 그냥 Controller에서 지연로딩같은 것들을 사용해도 된다.
  1. 코딩을 생각하면 키지만, 성능을 생각하면 끄는게 맞다.
  1. 고객 서비스를 많이 제공하는 실시간, API 서버등은 끄고 간다. 근데 admin System은 많이 안쓰기 때문에 그냥 키고 생성한다.