그냥 DTO만 쓴다 해도 어차피 lazyLoading은 하게 됨(DTO에서 참조하는 테이블의 필드에 접근 한 경우 프록시가 초기화 되기 때문)
그렇다고 Eager로 설정한다면, 어차피 쿼리는 나가고 성능은 성능대로 안좋고 예측할 수 없는 쿼리가 나가기도 함(연관관계 때문)
fetch join을 사용해서 해결 해야 함
jpql에서 join fetch는 연속해서 여러개 쓸 수 있음
이렇게 되면 그냥 쿼리 한방으로 원하는 엔티티들을 다 가져올 수 있음
즉, order -member,delivery인 상황에서 member와 delivery를 한번에 다 가져오면 쿼리가 더 나갈 이유가 없는 것
여기서 뭐 조인한 애들에서 필요없는 필드까지 데리고 오는 약간의 낭비는 발생하게 됨
이걸 해결하기 위해 JPA에서 DTO로 바로 뽑는 것 (약간의 성능 최적화 가능)
select에서 new DTO경로(생성자에 넣을 값) 이렇게 하고 join으로 필요한 애들 다 데리고 오는 것
하지만 이렇게 하는게 무조건 적으로 좋다고는 할 수 없다
외부의 껍데기를 조작하지 않고 내부에서 join fetch사용하는 것은 다른 곳에서도 유연하게 사용할 수 있지만
이렇게 사용하는 경우 성능적으로 좀 더 최적화는 되지만, fit하게 사용되는 메소드이므로 유연한 확장이 불가능 하다
지저분하기도 좀 더 지저분하고..
정리하자면, select절에서 원하는 데이터를 직접 선택하니 DB→애플리케이션간 네트워크 용량을 최적화할 수 있지만 생각보다 미비한 효과이고, 리포지토리 재사용성이 떨어지며 API스펙에 맞춘 코드가 리포지토리에 들어간다는 단점이 존재한다
논리적으로 보면 api spec이 바뀐다 → Repository 코드가 바뀐다 (계층간 분리가 되지 않았다)
이런 tradeOff가있는데, 뭐 .. 대부분은 fetch join과 성능 차이는 나지 않는다.
막 데이터가 엄청나게 크고 select에 사용되는 필드가 너무나도 많다면.. 사용을 고려해볼 필요가 있긴 함
영한님은 뭐 이런 최적화를 위한 쿼리는 따로 패키지 파서 repository를 따로 배치한다고 하심
API개발 고급- 컬렉션 조회 최적화
xToMany 관계를 조회 해 보자 !
DB입장에서 일대다 관계를 조인하게 되면 데이터가 뻥튀기가 된다. →최적화가 어려워진다.
마찬가지로 엔티티 리스트 직접 노출하는 경우 뭐 프록시 강제 초기화 시켜주고 리턴하면 되긴 하지만 이러면 역시 안!된!다!
어떤 엔티티를 DTO로 변경했는데 이 DTO 내에 또 엔티티가 존재한다면 해당 엔티티 또한 DTO로 변환해야 한다
Address같은 값 형은 노출해도 됨
만약 주문이 2개고 주문 당 orderitem이 2개면 둘을 조인하면 row가 4개가 된다 ㅇㅇ
그러면 jpa에서 orderItem을 fetch join으로 해서 order를 가져오면 이것도 4개가 됨
주소값까지 같은 order객체가 두배가 되니 의미 없는 order가 발생한다
select 절에 distinct를 추가하면 이런 중복을 제거할 수 있긴 함
사실 DB에서는 distinct가 있어도 다른 attribute의 값이 다르니 구별이 되어 똑같은 값을 리턴함
대신, JPA에서 DB에서 받아온 값을 들고 Order객체에 대해서만 중복을 확인하고 제거하는 것
💡 컬렉션 페치 조인을 하는 경우 , 페이징을 못하게 된다(메모리 페이징한다 ! →매우위험)
경고 로그 띄우고 실제 DB에서 페이징을 하는게 아니라 메모리에서 처리한다
데이터가 많은 상태로 이걸 해버리면 Out of Memory 뜨고 죽어버리는거임
생각해보면, 컬렉션 조회를 하면 중복된 order가 존재하게 됨. 즉, 예시 상황에서 똑같은 주문이 2개씩 들어있음. 그러면 페이징을 하게 되면 주문을 1번 인덱스부터 1개 뽑아서 페이징하면 상식적으로 1번 인덱스(2번째 주문)이 나와야 되는데, 실제 DB에는 1번째 주문이 1번 인덱스에 위치하기에(중복) 잘못된 페이징이 된다. 이 때문에, JPA가 경고 문구를 띄우고 자기가 메모리 써서 중복제거하고 페이징하는 것 </aside>
💡 컬렉션 페치 조인은 1개만 사용할 수 있음, 컬렉션 둘 이상에서 페치 조인을 사용하면 데이터가 부정합하게 조회될 수 있기 때문
컬렉션 조회 (ToMany)에서 페이징을 적용하는 방법
일대다 관계에서 1을 기준으로 페이징하는 것이 목적인데 데이터는 다를 기준으로 row가 생성됨
즉, order를 기준으로 페이징하고 싶은데 orderitem기준으로 페이징이 됨
방법
ToOne관계를 모두 페치조인한다 → 어차피 ToOne관계는 row를 증가시키지 않으니 페이징 쿼리에 영향을 주지 않음
Order → Delivery가 ManyToOne이니 추가해도 영향이 없다
여기서 Delivery가 또 뭐 Post를 ManyToOne관계로 가지면 이것도 같이 조회해도 영향 X
컬렉션은 그냥 지연 로딩을 사용해서 조회
지연 로딩 성능을 최적화 한다
hibernate.default_batch_fetch_size 설정 : 전역 설정
주로 이렇게 전역 설정을 사용한다.
@BatchSize 설정 : 개별(엔티티별) 최적화
order → orderitem → item 이렇게 1nm 인 관계를 배치를 이용하면(물론, 각 n,m이 설정한 배치 크기보다 작다는 가정하에) 1+1+1 이렇게 쿼리의 수가 훨씬 줄어들게 됨
이 방법의 장점
v3처럼 페치조인으로 데리고온 경우 어쨌든 DB에서 데이터를 데리고 올 때 데이터의 중복은 제거되지 않은 상태로 오게된다
따라서, DB에서 데이터를 전송하는 양이 많아진다
근데 이렇게 지연 로딩 최적화를 걸면 중복된 데이터를 애초에 만들지 않기에 DB데이터 전송량이 감소하게 된다.
그냥 생짜로 짠 v1과 비교하면 1+N → 1+1로 쿼리 호출 수가 줄어든다.
v3와 달리 페이징이 가능하다.
tradeoff
batch size크기를 보통 100~1000으로 설정하는데, 100로 하면 호출하는 쿼리의 수가 증가하게 되고 1000으로 하면 DB에서 1000개를 한번에 불러오니 순간적으로 부하가 발생하게 됨 → 순간 부하를 견딜 수 있을 정도로 설정해야한다.
약간 뭐, 연탄 나르는데 한번에 5장 주는거랑 10장주는 느낌이랄까.. 많이주면 들기 힘들지만 후딱 끝나지
DTO 직접조회 with 컬렉션
ToOne은 join ToMany는 loop돌면서 join
ToOne인 엔티티들은 그냥 조인해서 데리고오고
ToMany인 엔티티들은 루프를 돌면서 컬렉션의 갯수 N번만큼 쿼리를 날려 데리고 온다
즉. N+1번 의 쿼리를 날림
컬렉션 조회 최적화
ToOne 모두 한번에 join해서 들고오기
해당 Order의 id들을 들고 in절을 이용해서 한번에 orderItem을 다 들고오기
받아온 orderItemDto List를 stream의 collect.groupingBy를 이용해서 Map에 넣기
order에서 setOrderItem이건 이제 map에서 orderId로 꺼내서 넣어주면 됨
즉, ToOne에 대한 쿼리1번, In절에 대한 쿼리 1번 총2번의 쿼리가 발생
플랫 데이터 최적화
필요한 애들 전부 다 join 한다
이걸 통째로 받는 DTO를 짜서 이 데이터들을 받는다
이걸 이용해서 필요한거 슉슉 넣어서 원하는DTO를 만들어 낸다(노가다 작업)
쿼리는 총 1번밖에 나가지 않음
근데, ToMany를 조인했으니 데이터의 중복이 발생하고 데이터가 뻥튀기 됨
그래서 상황에 따라 오히려 컬렉션 조회 최적화 보다 더 느릴 수 있음
그리고, 원하는 스펙의 dto로 중복제거하고,., 매핑하고 이런 노가다를 해야 함
페이징도 불가능하다.(데이터 중복 때문에 원하는 페이징 결과가 만들어지지 않게 됨)
정리
컬렉션 조회 방법
엔티티로 조회한다
그대로 반환 : XXXXX,V1
조회 후 DTO로 변환 : V2
페치 조인으로 최적화 : V3
컬렉션 페이징과 한계 돌파 : V3.1
컬렉션을 페치 조인하면 페이징이 안되니
ToOne관계는 페치조인으로 최적화 하고
컬렉션은 지연 로딩을 유지하는 대신 배치 사이즈를 줘서 한번에 좀 크게 데리고 오는 것
DTO로 직접 조회한다
JPA에서 DTO 직접 조회 : V4
단건 조회의 경우 코드가 간단하기에 이 방식이 좋다
컬렉션 조회 최적화 : V5
IN절을 활용해 메모리에 미리 조회해서 최적화
코드가 복잡하다..그래도 쿼리의 수가 압도적으로 줄어든다.
플랫 데이터 최적화 : V6
전부 몽땅 다 조인한 결과를 원하는 스펙에 맞게 가공
V5와 성능 차이도 미미하고 노가다는 많이 필요해지며 코드도 복잡하다
권장 순서
엔티티 조회 방식으로 접근
페치조인
페이징 필요 시 배치 사이즈 주고 컬렉션은 지연로딩
해결이 안되면 DTO직접 조회
DTO직접 조회도 안되면 NativeSQL이나 JdbcTemplate사용
사실 엔티티 조회 방식으로 접근해서 페치조인 정도만 사용해줘도 거의 다 해결이됨
정말 트래픽이 많은 경우 다른 방식으로 최적화하는 것 보단 캐시를 사용하는게 좋음
참고로, 엔티티를 직접 캐시에 올리는 것은 바람직 하지 않음
API개발 고급-실무 필수 최적화
Open Session In View : OSIV -하이버네이트 에서 만든 것
JPA에선 Open EntityManager In VIew이긴 한데, 관례상 OSIV라 함
spring-jpa.open-in-view : (true가 기본 값)
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
OSIV를 켜면 이런 경고가 뜸
경고를 남기는 것은 당연히 주의가 필요하다는 뜻
JPA가 데이터베이스 커넥션을 가지고 오는 시점은 데이터 베이스 트랙잭션을 시작할 때이다.
뭐 서비스나 레포지토리 등등에서 뭐 transactional 어노테이션을 붙여 트랜잭션을 시작 함
또, 끝나는 시점은 OSIV설정에 따라 다름
OSIV가 켜져있다면
Transaction이 끝난 후에도 반환을 하지 않음
서비스 계층에서 트랜잭션 끝나고 컨트롤러에서도 또 lazy loading이 일어나거나 할 수 있음
즉, 영속성 컨텍스트가 트랜잭션이 끝났다고 사라지면 안됨
그래서, OSIV가 켜져있다면 고객의 요청이 들어왔을 때 api 컨트롤러면 완전히 반환할 때 까지 살아있고
뷰템플릿에 쓰이는 경우 렌더링이 다 완성되고 response가 유저에게 다 나간뒤에 사라짐