OSIV와 성능 최적화
- OSIV(Open Session In View) - 하이버네이트
- Open EntityManager In View - JPA
- JPA에서 엔티티 매니저가 하이버네이트에서는 Session이다. 예전에는 JPA가 없었고 하이버네이트만 존재했다.
- 그래서 OpenEntityManager In View 도 관례쌍 OSIV로 부른다.
- OSIV란, 영속성 컨텍스트를 뷰까지 열어두는 기능이다. 영속성 컨텍스트가 살아있으면 컨트롤러와 뷰에서 지연 로딩을 사용할 수 있다.
스프링 부트와 JPA를 같이 실행하면 이런 문구를 볼 수 있다.
WARN 22764 --- [ 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로 켜져있다.
📌 application,yml
spring:
jpa:
open-in-view : true
이렇게 설정을 지정할 수 있다.
1. spring.jpa.open-in-view : true
트랜잭션이 시작할 때 데이터베이스 커넥션을 가져온다.
OSIV true 전략은 최초 데이터베이스 커넥션 시작 시점부터 API 응답이 끝날때까지 또는 뷰 렌더링이 끝날떄 까지 영속성 컨텍스트와 데이터베이스 커넥션을 유지한다. 그래서 View Template나 API 컨트롤러에서 지연 로딩이 가능하다.
지연 로딩은 영속성 컨텍스트가 살아있어야 가능하고, 영속성 컨텍스트는 기본적으로 데이터베이스 커넥션을 유지한다. 결국 뷰나 컨트롤에서 지연 로딩을 사용할 수 있다는 것이 큰 장점이다.
그러나 이 전략은 너무 오랜기간 동안 데이터베이스 커넥션 리소스를 사용하기 때문에 실시간 트래픽이 중요한 애플리케이션에서는 DB 커넥션이 모자랄 수 있다. 이것은 결국 장애로 이어진다.
예를 들어서 컨트롤러에서 외부 API를 호출하면 외부 API 대기 시간만큼 커넥션 리소스를 반환하지 못하고 유지해야 한다.
OSIV를 키면 성능은 안좋지만 개발하기 편하다.
2. spring.jpa.open-in-view : false
OSIV를 종료하는 전략이다.
OSIV를 끄면 트랜잭션을 종료할 때 영속성 컨텍스트를 닫고, 데이터베이스 커넥션도 반환한다. 따라서 커넥션 리소스를 낭비하지 않는다.
OSIV를 끄면 모든 지연 로딩을 트랜잭션 안에서 처리해야 한다. 따라서 지연 로딩 코드를 모두 트랜잭션 안에서 처리해야한다는 단점이 있다.
그리고 View Template에서 지연로딩이 동작하지 않는다.
결론적으로 트랜잭션이 끝나기 전에 지연 로딩을 강제로 호출하거나 또는 페치 조인을 사용해서 해결해야 한다.
🔍 코드 예시
📌 OSIVOrderDto
package bookbook.shopshop.service.query;
@Getter
public class OSIVOrderDto {
// 요구 사항
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private List<OSIVOrderItemDto> orderItems;
public OSIVOrderDto(Order order) {
orderId = order.getId();
name = order.getMember().getName();
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress();
orderItems = order.getOrderItems()
.stream()
.map(OSIVOrderItemDto::new)
.collect(Collectors.toList());
}
}
📌 OSIVOrderItemDto
package bookbook.shopshop.service.query;
import bookbook.shopshop.domain.OrderItem;
import lombok.Getter;
@Getter
public class OSIVOrderItemDto {
// 요구 사항
private String itemName; // 상품 명
private int orderPrice; // 주문 가격
private int count; // 주문 수량
public OSIVOrderItemDto(OrderItem orderItem) {
itemName = orderItem.getItem().getName();
orderPrice = orderItem.getOrderPrice();
count = orderItem.getCount();
}
}
📌 OSIVResult
package bookbook.shopshop.service.query;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class OSIVResult<T> {
T data;
}
📌 OrderQueryService
package bookbook.shopshop.service.query;
import bookbook.shopshop.domain.Order;
import bookbook.shopshop.repository.order.OrderRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class OrderQueryService {
private final OrderRepository orderRepository;
public OSIVResult osivOffSample() {
List<Order> orders = orderRepository.findAllWithItem();
for (Order order : orders) {
System.out.println("order ref = " + order + " id = " + order.getId());
}
List<OSIVOrderDto> result = orders.stream()
.map(OSIVOrderDto::new)
.collect(Collectors.toList());
return new OSIVResult(result);
}
}
- OSIV를 false로 했기 때문에 OrderApiController의 V3 버전에서 했던 DB 작업들을 다 서비스, 레포지토리 계층에서 해야한다.
- 그래서 따로 서비스 계층을 만들었고 V3 버전의 로직을 그대로 가져왔다.
- 왜냐하면 OSIV가 꺼져있기 때문에 트랜잭션 라이프사이클동안 DB 커넥션을 가지고 있기 때문이다.
📌 OrderApiController
package bookbook.shopshop.api;
@RestController
@RequiredArgsConstructor
public class OrderApiController {
private final OrderRepository orderRepository;
private final OrderQueryRepository orderQueryRepository;
private final OrderQueryService orderQueryService;
/**
* V3. 엔티티를 조회해서 DTO로 변환(fetch join 사용)
* - 페이징시에는 N(컬렉션) 조회를 포기해야한다.
*/
@GetMapping("/api/v3/orders")
public Result ordersV3() {
List<Order> orders = orderRepository.findAllWithItem();
for (Order order : orders) {
System.out.println("order ref = " + order + " id = " + order.getId());
}
List<OrderDto> result = orders.stream()
.map(OrderDto::new)
.collect(Collectors.toList());
return new Result<>(result);
}
/**
* OSIV off 버전
*/
@GetMapping("/api/v3.OSIVOfF/orders")
public OSIVResult semiOrdersV3() {
return orderQueryService.osivOffSample();
@Data
static class OrderDto {
// 요구 사항
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private List<OrderItemDto> orderItems;
public OrderDto(Order order) {
orderId = order.getId();
name = order.getMember().getName();
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress();
orderItems = order.getOrderItems()
.stream()
.map(OrderItemDto::new)
.collect(Collectors.toList());
}
}
@Data
static class OrderItemDto {
// 요구 사항
private String itemName; // 상품 명
private int orderPrice; // 주문 가격
private int count; // 주문 수량
public OrderItemDto(OrderItem orderItem) {
itemName = orderItem.getItem().getName();
orderPrice = orderItem.getOrderPrice();
count = orderItem.getCount();
}
}
@Data
@AllArgsConstructor
static class Result<T> {
T data;
}
}
- OSIV가 꺼져있는 상태에서는 semiOrdersV3() 처럼 DB 작업을 다 서비스 계층에서 이뤄지도록 해야한다. 컨트롤러에서는 서비스 계층을 호출만 하는 것이다.
- 지연로딩 호출과 페치 조인이 전부 서비스 계층에서 처리된다.
3. 커맨드와 쿼리 분리
실무에서는 OSIV를 끈 상태로 복잡성을 관리하는 것이 좋은 방법이다. 바로 Command와 Query를 분리하는 것이다.
참고 자료 : https://en.wikipedia.org/wiki/Command%E2%80%93query_separation
단순하게 분리하는 예시는 이렇다.
- OrderService
- OrderSeerivce : 핵심 비즈니스 로직
- OrderQueryService : 화면이나 API에 맞춘 서비스 (주로 읽기 전용 트랜잭션 사용)
보통 서비스 계층에서 트랜잭션을 유지한다. 두 서비스를 모두 트랜잭션을 유지하면서 지연로딩을 사용할 수 있다.
결국 고객 서비스의 실시간 API는 OSIV를 끄고, ADMIN 처럼 커넥션을 많이 사용하지 않는 곳에서는 OSIV를 킨다.
🧷 참고
주로 어드민 시스템과 API 배포를 따로 진행한다.
👀 참고 자료
'[JPA] > 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화' 카테고리의 다른 글
[JPA] API 개발 고급 - 컬렉션 조회 최적화 (일대다 관계) (0) | 2022.04.12 |
---|---|
[JPA] API 개발 고급 - 지연 로딩과 조회 성능 최적화 (x대일 관계) (0) | 2022.04.11 |
[JPA] API 개발 기본 (0) | 2022.04.10 |