실무에서 너무 성능이 안 나와서 튜닝을 해야 할 경우, 코드를 확인해 보면 대부분 아래에 나열해 둔 문제가 자주 발생한다고 한다.
- 지연 로딩과 조회 성능 최적화
- 컬렉션 조회 최적화
- 페이징과 한계 돌파
- OSIV와 성능 최적화
등록 및 수정에선 거의 문제가 발생하지 않고, 90% 정도 조회할 때 성능 문제가 발생한다. 따라서 조회용 샘플 데이터를 입력한 뒤 각 문제에 맞춰 API 성능을 최적화하는 방법을 학습한다.
이번 섹션에선 OSIV(Open Session In View)에 따라 성능을 최적화할 수 있는 방법을 학습한다. JPA에서 사용하는 EntityManager가 하이버네이트에선 Session으로 변경됐다. 따라서 JPA에선 Open EntityManager In View로 불려야 하지만 관례상 OSIV로 부른다.
OSIV와 성능 최적화
트래픽이 조금이라도 많은 서비스에서 장애가 났을 때 OSIV에 대해 제대로 이해하고 있어야 어떤 장애가 발생했는지 알 수 있다.
OSIV ON
application.yml에서 spring.jpa.open-in-view가 기본값이 true로 설정돼 있다. 스프링 부트 애플리케이션을 실행하면 보이는 로그를 잘 살펴보면, 이 기본값을 뿌리면서 아래 warn 로그를 남기는 걸 확인할 수 있다.
WARN 10736 --- [ 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
JPA 기본적으로 언제 DB 커넥션을 가지고 오고, 언제 DB 커넥션을 DB에 반환할까? 영속성 컨텍스트를 사용해 지연 로딩을 하든 1차 캐시를 사용하든, JPA의 이 모든 기능은 DB와 커넥션이 돼있어야 가능하다. 그렇기 때문에 영속성 컨텍스트와 JPA는 가장 밀접하게 일대일로 연관돼 있다.
- OSIV 전략은 트랜잭션 시작처럼 최초 DB 커넥션 시작 시점부터 API 응답이 끝날 때까지 영속성 컨텍스트와 DB 커넥션을 유지한다. 그래서 지금까지 View Template이나 API Controller에서 지연 로딩이 가능했던 것이다.
- API의 경우엔 API가 유저에게 반환될 때까지, 화면인 경우엔 View가 렌더링 될 때까지, 트랜잭션이 끝나더라도 응답을 만들어서 반환할 때까지 연결을 유지하고 DB에게 커넥션을 반환한 뒤 사라진다.
- 지연 로딩은 영속성 컨텍스트가 살아있어야 가능하고, 영속성 컨텍스트는 기본적으로 DB 커넥션을 유지한다. 이것 자체가 큰 장점이다.
그런데 이 전략은 너무 오랜 시간 동안 DB 커넥션 리소스를 사용하기 때문에, 실시간 트래픽이 중요한 애플리케이션에서는 커넥션이 모자랄(날아갈) 수도 있다. 결국 이것 때문에 장애로 이어지게 된다.
- 예를 들어 Controller에서 외부 API를 호출하면 외부 API 대기 시간만큼 커넥션 리소스를 반환하지 못하고 계속해서 유지해야 한다.
OSIV OFF
application.yml에서 spring.jpa.open-in-view를 false로 설정해 OSIV를 종료할 수 있다. OSIV를 끄면 트랜잭션을 종료할 때 영속성 컨텍스트를 닫고, DB 커넥션도 반환한다. 따라서 커넥션 리소스를 낭비하지 않는다.
- 그러나 OSIV를 끄면 모든 지연 로딩을 트랜잭션 안에서 처리해야 한다. 따라서 지금까지 작성한 많은 지연 로딩 코드를 트랜잭션 안으로 넣어야 하는 단점이 있다.
- 또, View Template에서 지연 로딩이 동작하지 않게 되므로 결론적으론 트랜잭션이 끝나기 전에 페치 조인을 사용하거나 해서 지연 로딩을 강제로 호출해 두어야 한다.
참고
OSIV를 false로 설정하고 지연 로딩이 필요한 API를 호출하면, 프록시를 초기화하는 코드에서 아래 500 에러가 뜬다.
could not initialize proxy [jpabook.jpashop.domain.Member#1] - no Session
커멘드와 쿼리 분리
실무에서 OSIV를 끈 상태로 복잡성을 관리하는 좋은 방법이 있다. 바로 Command와 Query를 분리하는 것이다. 다음 링크를 참고하자. 패키지까지 분리하면 깔끔하게 관리할 수 있다.
보통 비즈니스 로직은 특정 엔티티 몇 개를 등록하거나 수정하는 것이기 때문에 성능이 크게 문제 되진 않는다. 그런데 복잡한 화면을 출력하기 위한 쿼리는 화면에 맞춰 성능을 최적화하는 것이 중요하다. 그러나 그 복잡성에 비해 핵심 비즈니스 로직에 큰 영향을 주는 것은 아니다. 그래서 크고 복잡한 애플리케이션을 개발하는 경우엔 이 둘의 관심사를 명확하게 분리하는 선택은 유지 보수 관점에서 충분히 의미 있다. 단순하게 설명해서 아래처럼 분리하는 것이다.
- OrderService
- OrderService: 핵심 비즈니스 로직
- OrderQueryService: 화면이나 API에 맞춘 서비스로, 주로 읽기 전용 트랜잭션에서 사용
- 보통 서비스 계층에서 트랜잭션을 유지한다. 이 방식을 사용하면, 두 서비스 모두 트랜잭션을 유지하면서 지연 로딩을 사용할 수 있다.
@Transactional(readOnly = true)
@Service
@RequiredArgsConstructor
public class OrderService {
// 핵심 비즈니스 로직; 라이프 사이클 느림
}
///
@Transactional(readOnly = true)
@Service
@RequiredArgsConstructor
public class OrderQueryService {
// 화면이나 API에 맞춘 서비스 (주로 읽기 전용 트랜잭션 사용); 라이프 사이클 빠름
}
참고
강사님은 고객 서비스의 실시간 API는 OSIV를 끄고, ADMIN처럼 커넥션을 많이 사용하지 않는 곳에서는 OSIV를 키는 방법을 사용한다고 한다.