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로 둔다.
'스프링 (인프런) > 스프링부트' 카테고리의 다른 글
스프링 데이터 JPA 소개 (0) | 2023.07.17 |
---|---|
API 개발 고급 정리 (0) | 2023.07.15 |
API 개발 고급 - 컬렉션 조회 최적화 2 (0) | 2023.07.15 |
API 개발 고급 - 컬렉션 조회 최적화 1 (0) | 2023.07.15 |
API 개발 고급 - 지연 로딩과 조회 성능 최적화 (0) | 2023.07.14 |