실전! 스프링 부트와 JPA활용2
2023, Feb 26
spring
강의 소개
- 인프런, 김영한님
- 총 24강 395분
- 실무에 도움되는 것들로 구성되어있다. 극약처방 같은 느낌
Summary
API 개발 기본
1. API 스펙이랑 Entity가 1:1로 맵핑되어서 발생하는 문제
- 즉, Entity가 변경되면, API스펙도 변경이 수반되는 문제
- 조치
- Dto객체를 생성해서 Entity를 대신하도록 한다.
- 즉, 프레젠테이션 계층을 위한 Result 클래스를 생성한다
- API스펙에 다 노출하지 말고 필요한 것만 노출하도록 한다.
API 개발 고급
- 지연 로딩과 조회 성능 최적화
- API 스펙으로 엔티티를 그대로 리턴하면 발생하는 문제
- 엔티티가 fetch=LAZY일 때
- proxy를 통해 ByteBuddyInterceptor 발생하니까 객체 강제 초기화를 하거나 hibernate모듈 디펜던시 걸거나
- 지연로딩을 피하기 위해서 즉시로딩(eagerLoading)을 사용하면 안된다.
- 즉시로딩 때문에 연관관계가 필요 없는 경우에도 데이터를 항상 조회해서 성능 문제가 발생할 수 있다. 튜닝하기도 어려워진다.
- 항상 지연 로딩을 기본으로 하고, 성능 최적화가 필요한 경우에는 패치 조인(fetch join)을 사용하도록 한다.
- 객체간 양방향 연관관계가 있을 때 (Member와 Order객체가 서로 참조할 때)
- 한 곳을 jsonIgnore를 선언해줘야한다.
- 엔티티가 fetch=LAZY일 때
store에서 조회한 값을 도메인 객체로 변환하는 과정에서 발생하는 N번 쿼링
@Data static class SimpleOrderDto { public SimpleOrderDto(Order order) { orderId = order.getId(); name = order.getMember().getName(); orderDate = order.getOrderDate(); orderStatus = order.getStatus(); address = order.getDelivery().getAddress(); } }
- 쿼리가 총 1+N+N번 실행된다.
- order조회
- order → member 지연 로딩 조회 N번
- order → delivery 지연 로딩 조회 N번
- 조치방법
- lazy로딩으로 변경한다.
- 지연로딩은 영속성 컨텍스트에서 조회하므로, 이미 조회된 경우 쿼리를 생략한다.
패치조인으로 최적화한다.
public List<Order> findAllWithMemberDelivery() { return em.createQuery( "select o from Order o" + " join fetch o.member m" + " join fetch o.delivery d", Order.class ).getResultList(); }
- 엔티티를 fetch join을 사용해서 쿼리 1번에 조회
- 패치 조인을 order → member, order → delivery는 이미 조회된 상태이므로 지연로딩자체가 일어나지 않는다.
- 웬만한 jpa성능은 lazy 로딩 + 패치 조인으로 다 해결된다
- lazy로딩으로 변경한다.
- 쿼리가 총 1+N+N번 실행된다.
- JPA에서 DTO로 바로 조회
- 화면에서 호출이 많은 조회용 API인 경우, select 절을 통해 가져올 데이터만 가져오게 해서 dto로 바로 조회하는 방법을 사용하면 네트워크를 줄일 수 있다.
- 그래도 개선이 안된다면
- JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용한다.
- API 스펙으로 엔티티를 그대로 리턴하면 발생하는 문제
2. 컬렉션 조회 최적화 (젤 중요‼️)
- 패치 조인 최적화
- 패치 조인시 1:다 조인이므로 중복 데이터는 distinct를 통해 리턴하도록 한다.
- 단점은 페이징 불가
- 컬렉션 패치 조인은 1개만 사용할 수 있다. 컬렉션 둘 이상에 패치 조인을 사용하면 안된다. 페이지가 부정합하게 조회될 수 있다.
- 패치 조인시 1:다 조인이므로 중복 데이터는 distinct를 통해 리턴하도록 한다.
- 페이징 + 컬렉션 엔티티 조인 문제 해결 방법
- ToOne(OneToOne, ManyToOne)관계를 모두 패치조인한다. ToOne관계를 row수를 증가시키지 않으므로 페이징 쿼리에 영향을 주지않는다.
- 컬렉션은 지연 로딩으로 조회한다.
- 지연 로딩 성능 최적화를 위해 hibernate.default_batch_fetch_size(전역 설정), @BatchSize(개별 설정)를 적용한다
- 이 옵션을 사용하면 컬렉션이나 프록시 객체를 한꺼번에 설정한 size만큼 IN쿼리로 조회한다.
- 웬만하면 이 옵션을 통해서 성능 최적화가능하다. 데이터양이 많으면 패치조인보다 낫다.
- 쿼리 호출수가 1 +N에서 1+1로 최적화된다.
- 웬만하면 100~1000사이를 선택하도록 한다. IN절 파라미터가 1000개로 제한이 있기도 하고 1000개를 한번에 불러오면 순간 DB부하가 올라갈 수 있기 때문에 그러함
- 지연 로딩 성능 최적화를 위해 hibernate.default_batch_fetch_size(전역 설정), @BatchSize(개별 설정)를 적용한다
JPA에서 DTO 직접 조회
public List<OrderQueryDto> findOrderQueryDots() { return em.createQuery( "select new jpabook.jpashop.repository.order.query.OrderQueryDto(o.id, om.name)" + " from Order o" + " join o.member m" + " join o.delivery d", OrderQueryDto.class) .getResultList(); }
3. 정리
- 엔티티 조회 방식으로 우선 접근
- 패치 조인으로 쿼리 수를 최적화
- 컬렉션 최적화
- 페이징이 필요한 경우 fetchsize 옵션 사용으로 최적화
- 페이징이 필요하지 않은 경우 → 패치 조인 사용
- 엔티티 조회방식으로 해결이 안되면 DTO조회 방식 사용
- DTO조회 방식으로 해결이 안되면 NativeSQL or 스프링 JdbcTemplate을 사용한다.
- 캐시는 Entity에 캐시하면 안된다. 영속성 컨텍스트때문에 Entity에 캐시하게 되면 복잡해진다. 캐시는 Dto에 해야한다.
OSIV와 성능 최적화
- open session in view : 하이버네이트 (jpa가 없었을 때)
- open entitymanager in view : jpa 로 되나, 관례상 osiv라 한다
설정
spring.jpa.open-in-view: true (default)
- 서비스단에서 @Transactional 을 보통 선언해두고, 서비스계층에서 트랜잭션이 시작할 때 db connection을 가져오는데, api가 user에게 반환될 때까지 트랜잭션이 종료해도 영속성 컨텍스트는 살아 있다. 화면의 경우는 렌더링이 완료될 때까지 데이터 커넥션을 물고 있다.
- open session in view를 false로 하면 커넥션을 빨리 반환해주기 때문에 커넥션 리소스를 낭비하지 않는다. 다만
- osiv를 끄면
- 모든 지연로딩을 트랜잭션 안에서 처리해야 한다는 것
- view template에서 지연로딩이 동작하지 않는다는 것
- 결론적으로 트랜잭션이 끝나기 전에 지연 로딩을 강제로 호출해 두어야 한다.
- osiv를 끄면
- osiv를 false로 하고 이렇게 구성한다
- 커맨드와 쿼리를 분리한다.
- 예로 OrderService
- OrderService : 핵심 비즈니스 로직
- OrderQueryService: 화면이나 API에 맞춘 서비스(주로 읽기 전용 트랜잭션 사용하는 경우)