주문 + 배송정보 + 회원을 조회하는 API를 만들자
지연 로딩 때문에 발생하는 성능 문제를 단계적으로 해결해보자.
> 참고: 지금부터 설명하는 내용은 정말 중요합니다. 실무에서 JPA를 사용하려면 100% 이해해야 합니다. > 안그러면 엄청난 시간을 날리고 강사를 원망하면서 인생을 허비하게 됩니다.
일단 dto를 사용하지 않고 엔티티 자체를 반환하는 방식으로 할 때 이런 문제들이 생기므로 이렇게 쓰지 말라고 하는거다. 그래서 편하게 보면 됨.
public List<Order> findAllByString(OrderSearch orderSearch) {
String jpql = "select o from Order o join o.member m";
boolean isFirstCondition = true;
//주문 상태 검색
if (orderSearch.getOrderStatus() != null) {
if (isFirstCondition) {
jpql += " where";
isFirstCondition = false;
} else {
jpql += " and";
}
jpql += " o.status = :status";
}
//회원 이름 검색
if (StringUtils.hasText(orderSearch.getMemberName())) {
if (isFirstCondition) {
jpql += " where";
isFirstCondition = false;
} else {
jpql += " and";
}
jpql += " m.name like :name";
}
TypedQuery<Order> query = em.createQuery(jpql, Order.class)
.setMaxResults(1000);
if (orderSearch.getOrderStatus() != null) {
query = query.setParameter("status", orderSearch.getOrderStatus());
}
if (StringUtils.hasText(orderSearch.getMemberName())) {
query = query.setParameter("name", orderSearch.getMemberName());
}
return query.getResultList();
}
쿼리 하는것도 내가 쌩으로 하고 했을 때. 즉 원리를 몰랐을 때 언제든지 발생할 수 있는 문제들.
처음에 그냥 이것만 하고 하면 밑처럼 무한루프 돔.
이유는 양방향일 때 서로 json으로 만들려고 왔다갔다 하다 보니 이런거다. 그래서 양방향이라면 어느 한 쪽은 끊어줘야 한다.
이렇게 한 쪽은 @JsonIgnore를 하자.
근데 이래도 또 문제점이 발생함.
ByteBuddyIntercepter어쩌구인데 이게 뭐냐면 안의 속성 불러올 때 fetch가 지연로딩으로 불러와지지만 자바 객체는 실제 안에 값이 있어야 뭐라도 할 수 있다. 그래서 임시로 프록시 클래스를 지원해주는 라이브러리 중 여기선 bytebuddy를 사용하는건데 속성 값에 이 프록시를 지정한다음 실제로 불러올 때 이 프록시가 실제 db값을 불러와 바뀌는 방식으로 동작한다. 그래서 프록시로 안바뀔 때 불러오면 자바에서 속성 타입이 안맞다고 에러가 뜨는 것.
사실 저런 식으로 빈 공간을 메꾼다고 보면 된다.
그럼 이걸 어떻게 해결하냐. 저런 건 null로 채워서 문제를 처리해주는 외부 라이브러리 hibernate5module를 가져와 사용한다. 기본값은 null을 반환하게 하는거임.
만약 안의 내용까지 채워서 주고 싶다면 강제로 불러오게 설정하면 된다.
원하지 않는 정보까지 불러와지므로 다시 강제로 불러오게 하는 설정은 없애고, 원하는 반환 정보만 내가 직접 get을 사용해서 불러온 뒤 반환도 할 수 있다.
이렇게 엔티티 자체를 반환하게 하려고 하면 많은 문제에 부딪히고 보안상 위험하고 엔티티 속성이 바뀌면 요청 반환 스펙도 다 바뀌고 난리난다. 그래서 엔티티로 하지 말라고 이렇게 직접 보여준거다.
이제 dto로 해보자.
내가 dto에서 지정한 대로 응답이 나간다.
이제 또 남아있는 문제는 N+1 문제다. 함수 설명된걸 자세히 보면 Lazy 초기화가 있는데, 다른 엔티티 db 정보를 where로 가져오기 때문에 생기는 db 조회가 많다는걸 말하는 거다.
자세히 말하면 현재 주문 2개있고 각 주문마다 다 달라 영속성 컨텍스트에 없어 1 + 2 + 2 = 5가 되는데 이 숫자가 중요한게 아니고 어쨌든 일일히 where로 해서 불러오기 때문에 만약 100개라면 100번 더 하는 식으로 해서 난 한번 불러올 뿐인데 select를 막 300번씩 하는 성능 낭비가 있다는거다.
이런 문제는 fetch join으로 해결한다.
fetch join은 jpa에만 있는 개념이고 처음 불러올 때 앞으로 뭐가 자주 쓰일지 아니까 미리 같이 가져와 사용하는걸 정의하는 거다. 그래서 성능 문제도 해결함.
낭비를 더 줄일수도 있는데 일단 db에서 객체를 가져와 서비스에서 자바 객체로 한번 dto로 바꿔주고 반환했다. 이렇게 말고 db에서 직접 쏘게 할 수도 있음.
내가 원하는 것만 직접 적는다. 직접 엔티티를 매개변수로 넣는게 아니고 저렇게 펼친 이유는 엔티티를 넣으면 그 객체의 식별자가 들어가기 때문. 그래서 생성자 하나 새로 만들어서 했다.
그럼 원하는 select 애들만 가져왔다.
근데 이 v3와 v4는 서로 선택 문제다. 이 select 컬럼 조금 없다고 성능이 엄청 향상되는건 아니고, 대부분 join에서 성능을 잡아먹고 v3처럼 db에서 가져와 객체로써 직접 정제해서 반환하는 것이 함수 재사용성에 좋기때문에 선택해야 한다. 별로 많이 안불러오는거면 저렇게 막 하지 않고 객체로써 불러와 사용해도 되고, 막 시도때도 없이 몇 십만번 씩 사용하는 거라고 하면 저 select해서 가져오는 컬럼 갯수 만이라도 줄인다.
여기까지 해서 최적화가 안되거나 진짜 네이티브 쌩으로 해야 할 정도로 복잡한거면 스프링에서 제공해주는 네이티브 작성 방법이나 템플릿을 사용하면 된다.