본문 바로가기
spring

Optional, JPA N + 1, fetch

by 오우지 2021. 11. 26.

자바 8

Optional<T>는 예상치 못한 NullPointerException 예외를 메서드로 회피할 수 있다. 또한 null체크를 안 해줘도 돼서 코드가 깔끔해진다.

만약 참조변수의 값이 만에 하나 null이 될 수 있다면 ofNullable()메서드를 이용해 Optional 객체를 생성하는 것이 좋다.

Optional 객체 생성 ->

String str = "abc";
Optional<String> optVal = Optional.of(str);
Optional<String> optVal = Optional.of("abc");
Optional<String> optVal = Optional.of(null);                 -> NullPointerException
Optional<String> optVal = Optional.ofNullable(null);         -> ok

 

빈 옵셔널 객체를 만들려면 empty()를 사용하면 된다

객체 내에 값이 있는지 확인하기 위해서 isPresent()를 사용할 수 있다.

null이 아닌 값으로 Optional을 만들었을 때만 값이 존재한다.

Optional<String> opt = Optional.empty();//빈 객체로 초기화

 

Optional 객체의 값 가져오기

Optional<String> optVal = Optional.of("abc");
String str1 = optVal.get();                                //optVal의 값 반환, null이면 예외
String str2 = optVal.orElse("");                           //optVal의 값 null이면 ""
String str3 = optVal.orElseGet(String::new)                //람다. new String
String str4 = optVal.orElseThrow(NullPointerException::new)//널이면 예외 발생

 

empty.isPresent//Optional 객체의 값이 null이면 false, 아니면 true 반환

Optional.ofNullable(str).ifPresent(System.out::println);
널이 아닐때만 작업 수행

 

 

출처: http://tcpschool.com/java/java_stream_optional

       https://www.youtube.com/watch?v=W_kPjiTF9RI 

 

 

N + 1문제

JPA 사용 시에 Lazy로딩으로 설정하면 쿼리가 1 + N + N 번 실행된다. 예를들어 Order 테이블을 조회하는데 Member 테이블의 정보를 필요로 해서 조회하면 나는 쿼리를 한번 실행했지만 해당하는 Order의 갯수만큼 n번, delivery 테이블의 정보를 필요로 하면 또다시 해당하는 Order의 갯수만큼 n번 실행된다는 이야기다.

 

이는 fetch join으로 해결 가능하다

예를들어 Order의 정보를 가져올 때 JPA의 Repository에서

public List<Order> findAllOrder(){
  return em.createQuery(
  "select o from Order o" + 
  " join fetch o.member m" +
  " join fetch o.delivery d", Order.class
  ).getResultList();
}

다음과 같이 메서드를 선언해주면 추가적 쿼리 없이 한번에 정보들을 들고올 수 있게 해준다.

fetch join에 대한 공부는 좀 많이 해놓으면 좋다.

 

추가적으로 Repository단에서 Order에 대한 정보들을 가져오는 것 말고도 직접 쿼리를 사용해서 Dto만을 뽑아오는 메서드를 만들 수 있다. 성능 최적화 측면에서는 가장 좋다고 할 수 있지만 재사용성은 떨어진다. 또한, 리포지터리에 API 스펙에 맞춘 코드가 들어가게 된다.

 

우선 Dto를 만들고 new로 jpql의 결과를 Dto로 즉시 변환해준다. 그리고

public List<OrderSimpleQueryDto> findOrderDtos() {
        return em.createQuery("select new jpabook.jpashop.repository.OrderSimpleQueryDto(o.id, m.name, o.orderDate, o.status, d.address)" +
                        " from Order o" +
                        " join o.member m" +
                        " join o.delivery d", OrderSimpleQueryDto.class)
                .getResultList();
    }

다음과 같이 적어주면 된다.

 

1. 우선 엔티티를 DTO로 변환한다.

2. 필요하면 페치 조인으로 성능을 최적화한다.

3. 그래도 안되면 DTO로 직접 조회한다.

4. 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용한다.

 

지금까지는 1대1, 다대1의 관계만 했지만 컬렉션인 일대다를 조회하고 최적화하는 방법은 살짝 다르다.

위에서 했던 것 같이 fetch join으로 해결하려 하면 

public List<Order> findAllWithItem() {
        return em.createQuery(
                        "select o from Order o" +
                                " join fetch o.member m" +
                                " join fetch o.delivery d" +
                                " join fetch o.orderItems oi" +
                                " join fetch oi.item i", Order.class)
                .getResultList();
    }

orderItems의 정보를 끌어올 때 각 orderItems 갯수에 맞는 join이 일어나기 때문에 order는 2개였더라고 orderItems 갯수에 따라서 행이 여러개가 되는 비효율이 생기게 된다.

 

여기서 repository의 쿼리문 설정에서 select distinct o form Order o로 설정해주면 DB에 distinct 키워드를 날려주고 엔티티가 중복인 경우에(Order의 메모리 주소가 같은 경우) 중복을 걸러서 컬렉션에 담아주게 된다. 물론 각 행의 값이 같은 부분이 없기 때문에 DB에서는 걸러지는게 없고 API의 측면에서, 서버단에서 걸러주는 역할을 하게 된다.

또한, 이전에 했던 fetch join의 효과와 같이 한번의 조회쿼리에 연관돼 있던 몇개의 쿼리가 단 한번만 나가는 성능적 이점이 있다.

 

하지만 일대다를 페치조인 하는 순간 페이징이 불가능하다. 컬렉션에 대한 페치 조인이 돼 있는 상태에서 페이징을 하면 모든 데이터를 DB에서 읽어오고 메모리에서 그 내용이 많아지면 페이징을 하기 때문에 메모리 오버가 난다.

 

또한 컬렉션 패치 조인은 1개만 사용할 수 있다. 컬렉션 둘 이상에 페치 조인을 사용하면 안된다.

 

출처 : https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-JPA-API%EA%B0%9C%EB%B0%9C-%EC%84%B1%EB%8A%A5%EC%B5%9C%EC%A0%81%ED%99%94/dashboard

'spring' 카테고리의 다른 글

MVC-2 message, validation  (0) 2022.02.11
HTTP 웹 기본지식 - 1  (0) 2022.02.04
스프링 기본편 복습 - 2  (0) 2022.01.28
스프링 기본편 복습 - 1  (0) 2022.01.23
JPA @Embedded, @Lob, 생성자  (0) 2022.01.15