API 개발 고급 - 실무 필수 최적화
OSIV와 성능 최적화
OSIV(Open Session In View)란
•
OSIV(Open Session In View)는 영속성 컨텍스트를 뷰까지 열어두는 기능
◦
Open Session In View : Hibernate
◦
Open EntityManager In View : JPA
▪
관례상 OSIV라 한다.
•
영속성 컨텍스트가 유지되면 엔티티도 영속 상태로 유지된다.
•
OSIV ON
•
spring.jpa.open-in-view : true
•
영속성 컨텍스트와 Database Connection은 밀접하게 연결되어있다.
◦
기본적으로는 DB Transaction을 시작할 때, 영속성 컨텍스트가 DB Connection을 획득한다.
▪
예를 들면 Service의 @Transactional
◦
그렇다면 영속성 컨텍스트가 DB Connection을 DB에 반환하는 시점은 언제일까?
▪
Transaction 범위가 끝나도 영속성 컨텍스트를 살려둔다.
▪
User에게 완전한 Response가 나갈 때 까지 Connection을 유지한다.
•
API의 경우, API 응답이 끝날 때 까지
•
View의 경우, 화면이 반환될 때 까지
◦
위와 같은 이유 때문에, View Template이나 API Controller에서 지연 로딩이 가능했던 것이다.
◦
지연 로딩은 영속성 컨텍스트가 살아있어야 가능하고, 영속성 컨텍스트는 기본적으로 DB Connection을 유지한다. → 이것 자체가 큰 장점이다!
▪
엔티티를 적극 활용해서 Lazy Loading같은 기술을 컨트롤러나 뷰에서 적극 활용할 수 있다.
▪
중복 제거, 투명한 Lazy Loading → 코드의 유지보수성을 높이는데 기여
•
하지만, 이 전략은 너무 오랜시간동안 데이터베이스 커넥션 리소스를 사용하기 때문에, 실시간 트래픽이 중요한 애플리케이션에서는 커넥션이 모자랄 수 있다! (커넥션이 말라버린다.) → 이것은 장애 유발 요인이 된다.
◦
예를 들어서 컨트롤러에서 외부 API를 호출하면, 외부 API 대기시간만큼 커넥션 리소스를 반환하지 못하고 유지하여야한다. → OSIV의 치명적인 단점!
OSIV OFF
•
spring.jpa.open-in-view : false
•
OSIV 종료
◦
OSIV를 끄면 트랜잭션을 종료할 때 영속성 컨텍스트를 닫고, DB Connection도 반환한다.
▪
예를 들면 Service의 @Transactional 범위가 끝날 때
◦
따라서, 커넥션 리소스를 낭비하지 않는다!
▪
사용자가 많은 경우 리소스를 효율적으로 사용할 수 있다!
◦
모든 지연로딩을 트랜잭션 안에서 처리해야한다.
▪
지금까지 작성한 많은 지연 로딩 코드를 트랜잭션 안으로 넣어야하는 단점이 있다.
▪
Controller, View Template에서 지연 로딩이 동작하지 않는다.
▪
결론적으로, 트랜잭션이 끝나기 전에 지연 로딩을 강제로 호출해두어야한다!
•
OrderController
@GetMapping("/api/v1/orders")
public List<Order> ordersV1() {
List<Order> all = orderRepository.findAllByString(new OrderSearch());
for (Order order : all) {
order.getMember().getName(); // 지연로딩 -> Proxy 객체 조회
order.getDelivery().getAddress(); // 지연로딩 -> Proxy 객체 조회
List<OrderItem> orderItems = order.getOrderItems();
orderItems.stream().forEach(o -> o.getItem().getName()); // OrderItem의 Item 강제 초기화
}
return all;
}
Java
복사
•
application.yml
jpa:
open-in-view: false
YAML
복사
•
영속성 컨텍스트가 트랜잭션이 끝날 때 DB Connection을 반환했기 때문에 Controller단에서 Proxy 객체를 조회할 때 에러가 발생한다. (지연로딩이 동작하지 않는다.)
•
해결 방법
◦
Fetch Join을 사용해 DB 조회한 Data를 메모리에 올려서 사용한다.
◦
Transaction안에서 지연로딩을 사용한다.
▪
Query용 Service를 따로 분리하여 Transaction을 관리한다. (아래 참고)
커맨드와 쿼리 분리
•
실무에서 OSIV를 끈 상태로 복잡성을 관리하는 좋은 방법이 있다.
◦
바로 Command와 Query를 분리하는것이다.
•
보통 비즈니스 로직은 특정 엔티티 몇개를 등록하거나 수정하는 것이므로 성능이 크게 문제가 되지 않는다.
◦
그런데, 복잡한 화면을 출력하기 위한 쿼리는 화면에 맞추어 성능을 최적화 하는 것이 중요하다.
▪
하지만 그 복잡성에 비해 핵심 비즈니스에 큰 영향을 주는 것은 아니다.
◦
그래서 크고 복잡한 애플리케이션을 개발한다면, 이 둘의 관심사를 명확하게 분리하는 선택은 유지보수
관점에서 충분히 의미 있다.
•
OrderService
◦
Life Cycle에 따라 서비스를 분리한다. (애플리케이션의 크기가 큰 경우)
▪
OrderService: 핵심 비즈니스 로직
▪
OrderQueryService : 화면이나 API에 맞춘 서비스 (주로 읽기 전용 트랜잭션 사용)
→ 보통 서비스 계층에서 트랜잭션을 유지한다. 두 서비스 모두 트랜잭션을 유지하면서 지연 로딩을 사용할 수
있다.
참고
김영한님은 고객 서비스의 실시간 API는 OSIV를 끄고, ADMIN 처럼 커넥션을 많이 사용하지 않는
곳에서는 OSIV를 켜서 사용한다고 한다.
참고
OSIV에 관해 더 깊이 알고 싶으면 자바 ORM 표준 JPA 프로그래밍 13장 웹 애플리케이션과 영속성
관리를 참고하자.