하이버네이트 : Open Session In View
JPA는 Open EntityManager In View 라고 함.
트래픽 많은 서비스에서는 장애가 날 수 있음. 이걸 알아야 함.
스프링 시작할 때, 남기는 WARN이 하나 있다.
2023-11-17T06:33:36.261+09:00 WARN 8448 --- [ 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
spring.jpa.open-in-view (기본값은 true임)
이게 Open Session In View 옵션인데, 말 그대로 view에서 세션 열아둔다,
이게 무슨 이야기냐면,
왜 우리가 예를들어 트랜잭션이 있는 서비스에서 반환 받아서 컨트롤러에서 만져도 지연 로딩이 발생한다.
이것이 영속성 컨텍스트가 관리하고 있기 때문에 지연 로딩이 가능한 것이다.
Open Sessin In View는, 말 그대로 view까지 세션을, DB의 커넥트를 물고 있는 영속성 컨텍스트를 열어두는 것이다.
즉 클라이언트에게 응답하기 전까지 살아있다.
그 이후에 DB커넥션을 돌려주고 영속성 컨텍스트가 사라진다.
영속성 컨텍스트는 응답으로 나가기 전 까지 계속 살아있지만, 트랜잭션 상태에서면 수정이 가능하다. 나머진 읽는 것만 된다.
근데, DB커넥션을 오래 물고 있어서, 실시간 트래픽이 중요한 애플리케이션에서는 커넥션이 모자랄 수 있다. 그래서 장애로 이어질 수 있다.
만약 컨트롤러에서 외부 API를 호출하면 그 시간만큼 커넥션 리소스 반환 못하고 유지됨.
기본값이 true니, false로 할 수도 있다. false가 되면,
이렇게 된다.
트랜잭션에서만 영속성 컨텍스트에서 관리를 한다.
트랜잭션 범위에서만 영속성 컨텍스트가 살아있다가, 트랜잭션이 끝나면 DB 커넥션도 반환하고 영속성 컨텍스트도 사라진다.
즉, 엔티티들을 영속성 컨텍스트에서 관리하지 않기 때문에, 지연로딩을 할 수가 없다.
그래서, 트랜잭션 범위 안에 모든 지연로딩을 처리해야 한다.
컨트롤러에서 만져줘서 지연로딩 했던 것을, 트랜잭션 안으로 코드를 집어넣어야 한다.
그래서,
open-in-view를 끄고
@GetMapping("/api/v1/orders")
public List<Order> ordersV1(){
List<Order> result = orderRepository.findAllString(new OrderSearch());
for (Order order : result) {
order.getMember().getName();
order.getDelivery().getAddress();
List<OrderItem> orderItems = order.getOrderItems();
for (OrderItem orderItem : orderItems) {
orderItem.getItem().getName();
}
}
return result;
}
트랜잭션 바깥에서 만져서 LAZY 로딩을 유발하는 이 컨트롤러를 호출해 보면,
org.hibernate.LazyInitializationException: could not initialize proxy [jpabook.jpashop.domain.Member#1] - no Session
이렇게 에러가 뜬다.
프록시를 이니셜라이징 할 수 없다고 한다. 세션이 없다고 한다.
그 세션은 영속성 컨텍스트를 말하나 보다.
즉, 영속성 컨텍스트가 없기에 (영속성 컨텍스트에 엔티티가(혹은 직접 조회한 Dto가) 포함되어 있지 않기에) 프록시를 초기화 할 수 없다.
저 프록시 초기화, 즉 지연 로딩은 영속성 컨텍스트에서 해주는 거다. 그런데 그 세션이 없다.
해결방법은 open-in-view를 키던 만져주는 걸 트랜잭션 범위 안에 넣든 join fetch를 써서 지연 로딩을 무시하고 한방에 가져오던 하면 된다.
해결 방법 중 하나는 이제 계층을 하나 더 넣는건데,
@GetMapping("/api/v1/orders")
public List<Order> ordersV1(){
List<Order> result = orderRepository.findAllString(new OrderSearch());
for (Order order : result) {
order.getMember().getName();
order.getDelivery().getAddress();
List<OrderItem> orderItems = order.getOrderItems();
for (OrderItem orderItem : orderItems) {
orderItem.getItem().getName();
}
}
return result;
}
원래 이렇게 컨트롤러에서 다 만져줬다면, open-in-view를 끈 상태에서는,
이런 식으로 서비스 계층을 하나 더 만든다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class OrderQueryService {
private final OrderRepository orderRepository;
public List<Order> ordersV1(){
List<Order> result = orderRepository.findAllString(new OrderSearch());
for (Order order : result) {
order.getMember().getName();
order.getDelivery().getAddress();
List<OrderItem> orderItems = order.getOrderItems();
for (OrderItem orderItem : orderItems) {
orderItem.getItem().getName();
}
}
return result;
}
}
여기서 트랜잭션을 주면된다.
그럼 저 repository안에 있는 트랜잭션과는 전파되어 함께 간다고 보면 된다.
https://qwefdg3.tistory.com/667
https://qwefdg3.tistory.com/670
그렇게 되면 여튼 트랜잭션 범위가 합쳐져 결국 저 범위동안은 영속성 컨텍스트가 살아있게 된다.
@GetMapping("/api/v1/orders")
public List<Order> ordersV1(){
List<Order> result = orderQueryService.ordersV1();
return result;
}
컨트롤러에서 저걸 호출해 주면 된다.
보통 컨트롤러에선 트랜잭션을 안쓰기 때문에, API나 화면에 맞춘 별도의 쿼리서비스 계층들을 만든다.
보통 핵심 비즈니스 로직은 바뀌지 않는 반면, 저런 API나 화면에 맞춘 기능들은 자주 바뀐다. 경영? 적인 관점에서 봤을 때 생명 주기가 다르다.
그래서, 핵심 비즈니스 로직과 저런 화면이나 API에 맞춘 것을 나누는 것이 좋다.
핵심 비즈니스 로직은 정말 그냥 완전 기초가 되는, 핵심적인 것들. 정말 비즈니스 로직인 것 도 있고(입금, 출금 등의) 때에 따라선 거의 리포지토리 대변인이 될 때도 있다.
근데 화면이나 API에 맞춘 것은 좀 임기응변? 그냥 때에 따라 맞추는? 그런 느낌이다.
그래서 이 두개를 분리하자.
OrderService라면,
OrderService : 핵심 비즈니스
OrderQueryService : 위 처럼 화면이나 API 스펙에 맞추는, 주로 읽기 전용 트랜잭션으로 함.
아키텍처에 따라선
컨트롤러, 애플리케이션 서비스, 도메인 서비스, 리포지토리
이런 식으로 계층을 하나 더 가져가는 경우가 있다.
도메인 서비스가 핵심비즈니스 로직같고, 애플리케이션 서비스가 애플리케이션에 맞춘, 화면이나 API에 맞춘 서비스 같다.
그래서 Open Session In View를 키냐 끄냐..
트래픽이 많은 실시간 API에서는 끄고, admin처럼 connection을 많이 사용하지 않는 곳에서는 킨다고 한다.
사람에 따라서는,
클라이언트가 요청한 정보는 서비스에서 책임지고 완성하고, 컨트롤러로 응답하며,
뷰나 컨트롤러에서는 전달받은 데이터를 신뢰하고 응답하는 것에만 집중하는 것이 좋다고 생각하는 사람도 있음.
'스프링데이터 + JPA > API 개발' 카테고리의 다른 글
16. Dto로 조회. 플랫 데이터 최적화 쿼리 하나 (0) | 2023.11.16 |
---|---|
15. Dto 직접조회 컬렉션 최적화 (0) | 2023.11.16 |
14. 컬렉션 Dto로 직접 조회 (0) | 2023.11.16 |
13. 컬렉션 페이징 한계돌파 (0) | 2023.11.15 |
12. 컬렉션 -> 엔티티 join fetch 최적화 (0) | 2023.11.15 |