카테고리 없음

예약 리스트 조회 API 최적화

o3oppp 2024. 11. 3. 16:07
엔티티를 DTO로 변환
@GetMapping("/api/v2/reserves")
public List<ReserveDto> reserveV2(){
    List<Reserve> reserves = reserveRepository.findAllByString(new ReserveSearch());
    List<ReserveDto> collect = reserves.stream()
            .map(r -> new ReserveDto(r))
            .collect(Collectors.toList());
    return collect;
}
...
@Data
static class ReserveDto{

    private Long reserveId;
    private String name;
    private LocalDateTime reserveDate;
    private ReserveStatus reserveStatus;
    private Address address;
    private List<ReserveShop> reserveShops;

    public ReserveDto(Reserve reserve) {
        reserveId = reserve.getId();
        name = reserve.getMember().getName();
        reserveDate = reserve.getReserveDate();
        reserveStatus = reserve.getStatus();
        address = reserve.getMember().getAddress();
        reserve.getReserveShops().stream().forEach(r -> r.getShop().getName());
        reserveShops = reserve.getReserveShops();
    }
}

API 호출 결과

  • DTO 내부에 ReserveShop 엔티티 존재
  • 엔티티에 대한 의존도를 끊어야 함

엔티티를 DTO로 변환(엔티티 의존도 끊음)
@Data
static class ReserveDto{

    private Long reserveId;
    private String name;
    private LocalDateTime reserveDate;
    private ReserveStatus reserveStatus;
    private Address address;
    private List<ReserveShopDto> reserveShops;

    public ReserveDto(Reserve reserve) {
        reserveId = reserve.getId();
        name = reserve.getMember().getName();
        reserveDate = reserve.getReserveDate();
        reserveStatus = reserve.getStatus();
        address = reserve.getMember().getAddress();
        reserveShops = reserve.getReserveShops().stream()
                .map(reserveShop -> new ReserveShopDto(reserveShop))
                .collect(Collectors.toList());
    }
}

@Data
static class ReserveShopDto {
    private String shopName;
    private int price;
    private int count;
    public ReserveShopDto(ReserveShop reserveShop) {
        shopName = reserveShop.getShop().getName();
        price = reserveShop.getReservePrice();
        count = reserveShop.getReserveCount();
    }
}
  • 의존도를 끊기 위해 ReserveShop 전용 DTO 생성

API 호출 결과

  • 엔티티 의존도는 끊어졌으나 1+N 문제 발생(로그 생략)
  • SQL 실행 수 (최악의 경우)
    • reserve 1번 조회
    • member, reserveShop N번 조회 (reserve 조회 수 = N)
    • shop N번 조회 (reserve 조회 수 = N)
  • 지연 로딩은 영속성 컨텍스트에 있으면 영속성 컨텍스트에 있는 엔티티를 사용하고, 없으면 SQL을 실행
    • 같은 영속성 컨텍스트에서 이미 로딩한 member 엔티티를 추가로 조회 시 SQL을 실행하지 않음
    • 해당 경우 reserve에서 조회한 member가 동일한 경우 N = 1

페치 조인 최적화
public List<Reserve> findAllWithShop() {
return em.createQuery(
        "select distinct r from Reserve r" +
                " join fetch r.member" +
                " join fetch r.reserveShops rs" +
                " join fetch rs.shop s", Reserve.class)
        .getResultList();
}
...
@GetMapping("/api/v3/reserves")
public List<ReserveDto> reserveV3(){
List<Reserve> reserves = reserveRepository.findAllWithShop();

List<ReserveDto> collect = reserves.stream()
        .map(r -> new ReserveDto(r))
        .collect(Collectors.toList());
return collect;
}
...
@Data
static class ReserveDto{

    private Long reserveId;
    private String name;
    private LocalDateTime reserveDate;
    private ReserveStatus reserveStatus;
    private Address address;
    private List<ReserveShopDto> reserveShops;

    public ReserveDto(Reserve reserve) {
        reserveId = reserve.getId();
        name = reserve.getMember().getName();
        reserveDate = reserve.getReserveDate();
        reserveStatus = reserve.getStatus();
        address = reserve.getMember().getAddress();
        reserveShops = reserve.getReserveShops().stream()
                .map(reserveShop -> new ReserveShopDto(reserveShop))
                .collect(Collectors.toList());
    }
}

@Data
static class ReserveShopDto {
    private String shopName;
    private int price;
    private int count;
    public ReserveShopDto(ReserveShop reserveShop) {
        shopName = reserveShop.getShop().getName();
        price = reserveShop.getReservePrice();
        count = reserveShop.getReserveCount();
    }
}

결과 로그

  • API 호출 결과는 동일하나 1+N 문제를 해결(쿼리 1번만 실행)

페이징 사용 시 경고 로그

  • 그러나 컬렉션 페치 조인(일대다에서 페치조인 후 페이징) 사용 시 페이징은 불가능
  • 하이버네이트는 경고 로그를 남기면서 모든 데이터를 DB에서 읽어오고, 메모리에서 페이징 해버림
  • 컬렉션 페치 조인은 1개만 사용할 수 있음. 2개 이상에 페치 조인 사용 시 데이터가 부정합하게 조회 가능

페이징 한계 해결
public List<Reserve> findAllWithMember(int offset, int limit) {
    return em.createQuery(
            "select r from Reserve r" +
                    " join fetch r.member m", Reserve.class)
            .setFirstResult(offset)
            .setMaxResults(limit)
            .getResultList();
}
@GetMapping("/api/v3.1/reserves")
public List<ReserveDto> reserveV3_page(
        @RequestParam(value = "offset", defaultValue = "0") int offset,
        @RequestParam(value = "limit", defaultValue = "100") int limit) {
    List<Reserve> reserves = reserveRepository.findAllWithMember(offset, limit);

    List<ReserveDto> collect = reserves.stream()
            .map(r -> new ReserveDto(r))
            .collect(Collectors.toList());
    return collect;
}
  • ToOne 관계는 row수를 증가시키지 않아 페이징 쿼리에 영향을 주지 않으므로 모두 페치 조인 적용
  • 컬렉션은 지연 로딩으로 조회
  • BatchSize 설정

  • 쿼리 호출 수가 1 + N -> 1 + 1로 최적화
  • 페치 조인 보다는 쿼리가 많이 실행되나 DB 전송량이 최적화(IN 으로 한번에 가져옴)
  • 컬렉션 페치 조인은 페이징이 불가능하지만 해당 방법은 페이징이 가능
  • BatchSize보다 크면 반복문 진행
  • ToOne 관계는 페치 조인으로 쿼리 수를 줄이고, 나머지는 BatchSize로 최적화