API 최적화 고급 정리

API 최적화 고급 정리

생성일
Jan 2, 2023 05:05 AM
최종 편집 일시
Last updated October 16, 2024
태그
JAVA
SPRING
  • 엔티티 조회
    • 엔티티를 조회해서 그대로 반환: V1
    • 엔티티 조회 후 DTO로 변환: V2
    • fetch join으로 쿼리 수 최적화: V3
    • 컬렉션 페이징과 한계 돌파: V3.1
      • 컬렉션은 페치 조인시에는 페이징이 불가능
      • ToOne 관계는 페치 조인으로 쿼리 수 최적화
      • 컬렉션은 페치 조인 대신 지연 로딩을 사용, hibernate.default_batch_fetch_size와 @BatchSize로 최적화
    • DTO 직접 조회
      • JPA에서 DTO를 직접 조회: V4
      • 컬렉션 조회 최적화 → 일대다 관계인 컬렉션은 IN 절을 활용해 아예 ‘메모리’에서 미리 조회해서 최적화 : V5
      • 플랫 데이터 최적화 → JOIN 결과를 그대로 조회 후 애플리케이션에서 원하는 모양으로 직접 변환: V6
  • * 권장 순서 **
  1. 엔티티 조회 방식으로 우선 접근
    1. 페치조인 활용, 쿼리 수 최적화
    2. 컬렉션 최적화
      1. 페이징 필요 → hibernate.default_batch_fetch_size, @BatchSize로 최적화
      2. 페이징 필요 없는 경우 → 페치 조인 사용
  1. 엔티티 조회 방식으로 해결이 안되는 경우 DTO 조회 방식 사용
  1. DTO 조회 방식으로 해결이 안되면 NativeSQL or 스프링 JdbcTemplate 활용. (해도해도 안되는 경우…)
개발자는 성능 최적화와 코드 복잡도 사이에서 줄타기를 해야 한다.
보통은 성능 최적화는 단순한 코드를 복잡한 코드로 몰고간다.
엔티티 조회 방식은 JPA가 많은 부분을 최적화 해주기 때문에 단순한 코드를 유지하면서 성능 최적화가 되는 장점이 있다.
반면 DTO 조회 방식은 SQL을 직접 다루는 것과 유사하므로… 줄타기를 잘 해야 한다.

DTO 조회 방식의 선택지

  • DTO로 조회하는 방법도 각각 장단점이 있다. V4,5,6에서 쿼리가 단순히 한 번 실행된다고 해서 V6가 항상 좋은 방법인 것은 아니다.
  • V4는 코드가 단순함. Order가 한 건이면 OrderItem을 찾기 위한 쿼리도 한 번만 실행하면 된다.
  • V5는 코드가 복잡함. 여러 주문을 한꺼번에 조회하는 경우에는 V4보다는 V5가 더 좋다.
    • 예를 들어 Order 데이터 1000건 짜리를 조회한다면 V4에서는 1+1000번 쿼리가 날아가지만 V5 방식에서는 쿼리가 1 + 1 번만 날아간다. 따라서 규모가 커질수록 성능차이는 심해진다.
    • * 사실 default_batch_fetch_size 옵션을 사용하는 것만으로도 V5의 성능이 나온다.
  • V6는 쿼리 한번이라 매우 최적화된 것 맞지만 페이징이 불가능하다.

OSIV (Open Session In View)와 성능 최적화

JPA의 EntityManager = Hibernate의 Session
EntityManager이므로 OEIV가 맞지만 관례상 OSIV로 부른다.
spring.jpa.open-in-view: true
인 경우에 트랜잭션 시작처럼 최초 데이터베이스 커넥션 시작 시점부터 API 응답이 끝날 때까지 영속성 컨텍스트와 데이터베이스 커넥션을 유지한다. 이 때문에 지연 로딩이 가능했던 것임.
지연 로딩이라는 것은 영속성 컨텍스트가 살아있어야 가능하고, 영속성 컨텍스트는 기본적으로 데이터베이스 커넥션을 유지한다.
하지만 이게 켜져 있으면 너무 오랜시간동안 데이터베이스 커넥션 리소스를 사용하기 때문에 실시간 트래픽이 중요한 애플리케이션에서는 커넥션이 모자라게 될 수 있다. → 장애남.
OSIV가 꺼져 있을 경우에는 트랜잭션이 종료될 때 영속성 컨텍스트를 닫고, 데이터베이스 커넥션도 반환한다. 따라서 커넥션 리소르를 낭비하지 않는다.
OSIV를 끄면 모든 지연로딩을 트랜잭션 안에서 처리해야 한다. —> 지연 로딩 코드를 트랜잭션 안으로 집어 넣어야 하는 단점이 생긴다. ⇒ 트랜잭션이 끝나기 전에 지연 로딩을 강제로 호출해 두어야 한다.
notion image
영속상태가 아닌 경우에 lazy 로딩을 해버리면 could not initialize proxy 에러를 뱉어낸다.

해결 방법

OrderQueryService 등 화면에 맞춘 쿼리형 서비스 레이어를 만들어서 Controller에 들어 있던 lazy 관련된 로직을 다 옮겨버린다.
우리 코드는 아예 Domain 패키지 안에 들어가있기 때문에 이런 사항들이 제대로 고려가 되어 있는 상태라고 볼 수 있다.
고객 쪽 서비스는 보통 실시간이 중요하니까 OSIV를 끄고, ADMIN 처럼 커넥션을 많이 필요로 하지 않는 곳에서는 OSIV를 켠다.
동적 쿼리가 하고 싶을 때는 querydsl을 사용하면 된다.