본문 바로가기

스프링 (인프런)/스프링부트

OSIV와 성능 최적화

1. 정의

- 이름

    - 하이버네이트 : Open Session in View

    - JPA : Open EntityManager in View

 

 

 

2023-07-16 11:54:43.810  WARN 36992 --- [  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

- 스프링을 시작할 때마다 WARN 로그가 지속적으로 발생한다.

- 그 내용을 보면 spring.jpa.open-in-view가 디폴트로 실행되고 있음을 알 수 있다.

 

 

 

OSIV 전략

- 최초 데이터베이스 커넥션 시점부터 API 응답이 끝날 때까지 영속성 컨텍스트와 데이터베이스 커넥션을 유지한다.

 

- 위 그림을 예시로 들면, 서비스 계층에서 @Transactional이 걸려 있어서 트랜잭션이 시작하면서 DB 커넥션을 물고 온다.

- 그런데 단순히 @Transactional의 메서드를 벗어나는 순간 트랜잭션이 끝나고, DB 커넥션도 원래는 끝나는 게 정상이다.

 

@GetMapping("/api/v1/orders")
public List<Order> ordersV1() {
    List<Order> orders = orderRepository.findAllByString(new OrderSearch());
    for (Order order : orders) {
        order.getMember().getName();
        order.getDelivery().getAddress();

        // OrderItem 조회 추가됨
        List<OrderItem> orderItems = order.getOrderItems();
        orderItems.stream().forEach(o -> o.getItem().getName());
    }

    return orders;
}

- 그런데 그렇게 되면, 위와 같은 컨트롤러 계층 메서드가 getMember(), getDeliver()를 요청할 수 없다.

- 서비스 계층에서 회원과 배송은 프록시로 남아 있고, 컨트롤러 계층에서는 이제 DB에서 실제 객체를 들여올 수 없기 때문이다.

- 그런데 OSIV 전략을 사용하면 컨트롤러에서도 프록시를 실제 객체로 변경하는 것이 가능해진다.

 

 

 

- 그러나 이 전략은 DB 커넥션을 너무 오랫동안 물고 있는다.

- 결국 커넥션이 다중으로 발생하는 환경에서는 커넥션이 말라버릴 수 있다.

- 그래서 실시간 트래픽이 중요한 애플리케이션에서는 치명적인 단점이 될 수 있다.

- 예를 들어서 컨트롤러에서 외부 API를 호출하면 외부 API 대기 시간 만큼 커넥션 리소스를 반환하지 못하고유지해야 한다.

 

 

 

예시

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

- application.yml에서 spring.jpa.open-in-view를 false로 두었다.

 

 

 

@GetMapping("/api/v1/orders")
public List<Order> ordersV1() {
    List<Order> orders = orderRepository.findAllByString(new OrderSearch());
    for (Order order : orders) {
        order.getMember().getName();
        order.getDelivery().getAddress();

        // OrderItem 조회 추가됨
        List<OrderItem> orderItems = order.getOrderItems();
        orderItems.stream().forEach(o -> o.getItem().getName());
    }

    return orders;
}

- 그리고 postman에서 ordersV1()을 호출하도록 GET 명령어를 보냈다.

- ordersV1()에서는 getMember.getName()과 getDelivery().getAddress()에서 프록시 초기화를 요청한다.

- 그러나 OSIV가 false이기 때문에 DB 커넥션이 이때 이미 종료되었을 것이고, 따라서 에러가 발생한다.

 

- postman에서 반환받은 결과다.

- "trace"를 자세히 보면 'could not initialize proxy'라는 에러 메시지를 읽을 수 있다.

 

 

 

솔루션 - CQS ( command-query separation )

- 이전에도 다룬 적 있는 커맨드-쿼리 분리 전략은 데이터를 변경하는 커맨드와 조회하는 쿼리를 분리한다는 전략이다.

- 데이터를 변경하는 커맨드는 핵심 비즈니스 로직과 관련이 있다.

    - 핵심 비즈니스 로직은 변경이 거의 없다.

- 데이터를 조회해서 클라이언트에 반환하는 쿼리는 화면 혹은 API와 관련이 있다.

    - API는 버전에 따라 지속적으로 변경 및 수정해야 한다.

 

 

 

private final OrderQueryService orderQueryService;
@GetMapping("/api/v3/orders")
public List<OrderDTO> ordersV3() {
    return orderQueryService.ordersV3();
}

- 컨트롤러의 ordersV3()가 새로운 OrderQueryService에 의존하고 있다.

 

 

 

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

    @GetMapping("/api/v3/orders")
    public List<OrderDTO> ordersV3() {
        List<Order> orders = orderRepository.findAllWithItem();
        return orders.stream()
                .map(o -> new OrderDTO(o))
                .collect(toList());
    }
}

- OrderQueryService는 이전의 ordersV3()의 로직을 그대로 담고 있다.

- 다만, OrderDTO의 생성자에서 getMember().getName() 및 getDelivery().getAddress()하기 위해 @Transactional을 달았다.

- 참고로, OrderDTO와 OrderItemDTO는 원래 컨트롤러 계층에 있었는데, 여기서 쓰기 위해 서비스 계층으로 옮겼다.

 

- 결론적으로,

    - 핵심 비즈니스 로직은 OrderService에 두고

    - API는 OrderQueryService에 둔다.

 

 

 

OSIV 기준

- 커넥션이 많은 고객 서비스의 실시간 API는 OSIV를 false로 둔다.

- 커넥션이 적은 ADMIN은 true로 둔다.