Web/JPA

EP5. OSIV와 성능 최적화

 

 

OSIV (Open Session In View)

풀네임

하이버네이트 에서는 Open Session In View

JPA에서는 Open EntityManager In View 이다.

 

 

OSIV가 off 이면 트랜잭션을 종료할때 영속성 컨텍스트 닫히면서 데이터 커넥션도 반환한다. 그런데 OSIV가 on이면 트랜잭션이 끝난 이후에도 영속성 컨텍스트가 살아있고 데이터 커넥션 풀이 계속 존재한다. 그래서 지연로딩과 같은 전략을 사용할 수 있다.

 

 

장단점

OSIV on

장점 : 지연로딩을 사용할 수 있다.

단점 : 데이터커넥션 풀이 유지되는 리소스가 크다.

 

OSIV off

장점 : 데이터 커넥션 풀을 원하는 시간만 사용하기 때문에 자원의 낭비가 없다.

단점 : 지연로딩을 사용할 수 있고 사용하려면 트랜잭션이 끝나기 전에 지연로딩을 강제로 호출해야한다.

 

 

 

OSIV 켜기

기본적으로 OSIV는 디폴트가 켜져있는 것이다. 그래서 지금까지 지연로딩이 아무문제 없이 동작하고 있었다.

2021-04-09 14:56:32.876  WARN 81993 --- [           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

애플리케이션을 동작시키면 항상 WARN 로그가 발생했었다. default로 open-in-view가 켜져있으니, 데이터베이스는 뷰 렌더링동안 유지된다는 뜻으로 경고를 주는 것이다.

 

 

OSIV 끄기

OSIV를 끄려면 spring.jpa.open-in-view : false로 해주면 된다.

spring:
  datasource:
    url: jdbc:h2:tcp://localhost/~/jpashop
    username: sa
    password:
    driver-class-name: org.h2.Driver

  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
#        show_sql: true
        format_sql: true
        default_batch_fetch_size: 100
    open-in-view: false

logging.level:
  org.hibernate.SQL: debug
  org.hibernate.type: trace

 

그러면 OSIV on 일때와 달리 애플리케이션을 켜도 WARN 로그가 나오지 않는다.

 

대신에 지연로딩을 하면 실패한다.

    @GetMapping("/api/v1/orders")
    public List<Order> ordersV1() {
        List<Order> all = orderRepository.findAllByString(new OrderSearch());
        for (Order order : all) {
            order.getMember().getName();
            order.getDelivery().getAddress();
            List<OrderItem> orderItems = order.getOrderItems();
            orderItems.stream().forEach(o -> o.getItem().getName());
//            for (OrderItem orderItem : orderItems) {
//                orderItem.getItem().getName();
//            }
        }
        return all;
    }

 

order.getMember(), order.getDelivery 에서 지연로딩을 하는 코드를 실행했을때

{
    "timestamp": "2021-04-09T06:01:53.405+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "message": "",
    "path": "//api/v1/orders"
}

500 에러가 뜨고,

 

 

org.hibernate.LazyInitializationException: could not initialize proxy [jpabook.jpashop.domain.Member#1] - no Session
	at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:170) ~[hibernate-core-5.4.25.Final.jar:5.4.25.Final]
	at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:310) ~[hibernate-core-5.4.25.Final.jar:5.4.25.Final]
	at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:45) ~[hibernate-core-5.4.25.Final.jar:5.4.25.Final]
	at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:95) ~[hibernate-core-5.4.25.Final.jar:5.4.25.Final]

proxy를 초기화 할 수 없다는 에러가 발생한다.

 

OSIV를 off 했으므로 지연로딩을 할 수 없는 것이다.

 

 

 

 

OSIV를 끈 상태로 복잡성 관리하는 방법

 

지연로딩을 트랜잭션 안으로 들고가기 (커맨드와 쿼리 분리)

 

 

보통의 비즈니스 로직은 성능이 크게 문제가 되지 않는다.

그런데, 복잡한 화면을 출력하기 위한 쿼리는 화면에 맞게 성능을 최적화 하는 것이 중요하다.

그래서 이 둘의 관심사를 분리해놓아야 유지보수 관점에서 좋다.

 

OrderService

- OrderService : 핵심 비즈니스 로직

- OrderQueryService : 화면이나 API에 맞춘 서비스 (주로 읽기 전용 트랜잭션 사용)

 

 

 

서비스계층을 만들고 @Transaction 안에서 지연로딩을 하는 것이다.

컨트롤러는 서비스 계층의 해당 서비스를 호출만 한다.

 

 

OrderQueryService

package jpabook.jpashop.service.query;

import jpabook.jpashop.domain.Order;
import jpabook.jpashop.repository.OrderRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

import static java.util.stream.Collectors.toList;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class OrderQueryService {
    private final OrderRepository orderRepository;

    public List<OrderDto> ordersV3() {
        List<Order> orders = orderRepository.findAllWithItem();
        List<OrderDto> result = orders.stream()
                .map(OrderDto::new)
                .collect(toList());
        return result;
    }
}

 

 

OrderApiController

    @GetMapping("/api/v3/orders")
    public List<jpabook.jpashop.service.query.OrderDto> ordersV3() {
        return orderQueryService.ordersV3();
    }

 

 

open-in-view를 끄고 나서도 잘 동작한다.

[
    {
        "orderId": 11,
        "name": "userB",
        "orderDate": "2021-04-09T15:24:57.14582",
        "orderStatus": "ORDER",
        "address": {
            "city": "진주",
            "street": "2",
            "zipcode": "2222"
        },
        "orderItems": [
            {
                "itemName": "SPRING1 BOOK",
                "orderPrice": 20000,
                "count": 3
            },
            {
                "itemName": "SPRING2 BOOK",
                "orderPrice": 40000,
                "count": 4
            }
        ]
    }
]

 

 

 

 

 

 

결론

커넥션을 자주 사용하는 고객 실시간 API 서버는 OSIV를 끈다.

그러나 ADMIN처럼 커넥션을 많이 사용하지 않는 곳에서는 OSIV를 킨다.