카테고리 없음
예약 리스트 조회 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();
}
}
- 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 생성
- 엔티티 의존도는 끊어졌으나 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로 최적화