문제점
기존 관리자 페이지를 호출 시 페이지를 호출하는 시간이 너무 오래걸리는 문제가 발생했고
페이지 호출 시간 = 데이터의 개수 만큼의 페이지 지연이 발생되었다.
군산오름 주차장 기준 1461ms 가 소요되었다.
원인
기존 로직의 흐름에 문제가있었는데
다른 두 테이블의 데이터를 합쳐야 하고 검색조건에 맞는 결과만 내어줘야하기 때문에
두번의 DB호출에서 Pageable을 걸지 못했고
얻어온 데이터를 합치는 과정에서 검색조건을 필터링 했는데
그 결과 많은 쿼리 조회로 인해 페이지 조회가 느려진것이다.
기존 로직은 다음과 같다.
- 주차장의 모든 예약정보를 가져온다.
- 예약 정보 중 입차 한 기록이 있는지 DB에서 검색한다.
- 입차 했을경우 : 검색 조건에 맞는 데이터를 입차 차량으로 리스트에 추가한다.
- 입차 안했을경우 : 검색 조건에 맞는 데이터를 미 입차 차량으로 리스트에 추가한다.
- 리스트에 추가 된 차량을 정렬 조건에 맞춰 정렬한다.
- 리스트를 페이징화 시켜 리스트를 리턴한다.
- 기존 코드
// 예약시간이 종료되지 않은 예약 정보를 불러오기 List<ParkBookingInfo> parkBookingInfos = parkBookingInfoRepository.findAllByParkInfoIdOrderByStartTimeDesc(parkInfo.get().getId()); List<ParkMgtResponseDto> parkMgtResponseDtos = new ArrayList<>(); for (ParkBookingInfo p : parkBookingInfos) { Optional<ParkMgtInfo> parkMgtInfo = parkMgtInfoRepository.findByParkBookingInfoId(p.getId()); ParkMgtResponseDto parkMgtResponseDto; LocalDateTime startTime = p.getStartTime(); LocalDateTime exitTime = p.getExitTime(); long minutes = Duration.between(startTime, exitTime).toMinutes(); int charge = ParkingFeeCalculator.calculateParkingFee(minutes, parkOperInfo); if (parkMgtInfo.isPresent()) { if (state == 2 && parkMgtInfo.get().getExitTime() != null || state == 1 && parkMgtInfo.get().getEnterTime() != null) { continue; } parkMgtResponseDto = ParkMgtResponseDto.of(p.getCarNum(), parkMgtInfo.get().getEnterTime(), parkMgtInfo.get().getExitTime() , p.getStartTime(), p.getEndTime(), p.getExitTime(), parkMgtInfo.get().getCharge()); parkMgtResponseDtos.add(parkMgtResponseDto); } else if (state == 0 || state == 1) { if (state == 1 && p.getEndTime().isBefore(LocalDateTime.now())){ continue; } parkMgtResponseDto = ParkMgtResponseDto.of(p.getCarNum(), null, null , p.getStartTime(), p.getEndTime(), p.getExitTime(), charge); parkMgtResponseDtos.add(parkMgtResponseDto); } } String parkName = parkInfo.get().getName(); Long parkId = parkInfo.get().getId(); switch (sort) { case 0: Collections.sort(parkMgtResponseDtos, Comparator.comparing(ParkMgtResponseDto::getBookingStartTime, Comparator.nullsLast(Comparator.reverseOrder()))); break; case 1: Collections.sort(parkMgtResponseDtos, Comparator.comparing(ParkMgtResponseDto::getBookingEndTime, Comparator.nullsLast(Comparator.reverseOrder()))); break; case 2: Collections.sort(parkMgtResponseDtos, Comparator.comparing(ParkMgtResponseDto::getEnterTime, Comparator.nullsLast(Comparator.reverseOrder()))); break; case 3: Collections.sort(parkMgtResponseDtos, Comparator.comparing(ParkMgtResponseDto::getExitTime, Comparator.nullsLast(Comparator.reverseOrder()))); break; default: break; } int totalElements = parkMgtResponseDtos.size(); int fromIndex = (int) pageable.getOffset(); int toIndex = Math.min(fromIndex + pageable.getPageSize(), totalElements); List<ParkMgtResponseDto> pagedResponseDtos = parkMgtResponseDtos.subList(fromIndex, toIndex); Page page1 = new PageImpl(pagedResponseDtos, pageable, totalElements); return ParkMgtListResponseDto.of(page1, parkName, parkId, totalActualCharge, totalEstimatedCharge);
해결과정
예약정보, 입차 정보 테이블은 두개가 각각 다른 정보를 담고있고
Join을 사용하여 쿼리를 작성방법을 생각하던 중 Left Join을 사용하기로 했고
해당 쿼리를 적용했을때 두개의 테이블이 합쳐진 결과를 얻을 수 있었다.
SELECT *
FROM park_booking_info a
LEFT OUTER JOIN park_mgt_info b
ON b.park_booking_info_id = a.id where a.park_info_id ='70654'
이제 Join쿼리는 작성했으니 검색결과에 따른 동적쿼리를 작성해야 하는데
QueryDSL을 사용하면 동적쿼리를 생성하기 좋기때문에 사용하게 되었다.
QueryDSL 작성
ParkBookingInfoRepositoryCustom (최종)
public interface ParkBookingInfoRepositoryCustom {
Page<ParkBookingInfoMgtDto> findByMgtList(Long parkInfoId, int state, int sort, Pageable pageable);
}
ParkBookingInfoRepositoryImpl (초기)
@Repository
@RequiredArgsConstructor
public class ParkBookingInfoRepositoryImpl implements ParkBookingInfoRepositoryCustom {
private final JPAQueryFactory jpaQueryFactory;
private final QParkBookingInfo qParkBookingInfo = QParkBookingInfo.parkBookingInfo;
private final QParkMgtInfo qParkMgtInfo = QParkMgtInfo.parkMgtInfo;
@Override
public Page<ParkBookingInfo> findByMgtList(Long parkInfoId, int state, int sort, Pageable pageable) {
return jpaQueryFactory.selectFrom(qParkBookingInfo)
.leftJoin(qParkMgtInfo)
.on(qParkMgtInfo.parkBookingInfo.id.eq(qParkBookingInfo.id))
.where(qParkBookingInfo.parkInfo.id.eq(parkInfoId))
.fetch();
}
}
Projections.constructor
위의 코드에는 문제가있었는데 주차 예약 테이블, 입차 정보 테이블 두 테이블의 값을 원했지만
주차 예약 테이블의 값만 가져올수있었다.
해결방법을 찾던 중 Projections.constructor
를 알게되었고
Projections.constructor
은 QueryDSL에서 DTO객체의 생성자를 이용하여 매핑하는데 사용된다.
매핑할 DTO를 바로 생성해줬고 적용시켰다.
return jpaQueryFactory.select(Projections.constructor(ParkBookingInfoMgtDto.class,
qParkBookingInfo.carNum, qParkBookingInfo.startTime, qParkBookingInfo.endTime,
qParkBookingInfo.user.id, qParkBookingInfo.exitTime, qParkMgtInfo.parkBookingInfo.id,
qParkMgtInfo.enterTime, qParkMgtInfo.exitTime, qParkMgtInfo.charge))
.from(qParkBookingInfo)
.leftJoin(qParkMgtInfo).on(qParkBookingInfo.id.eq(qParkMgtInfo.parkBookingInfo.id))
.where(whereBuilder)
.orderBy(orderSpecifierList.toArray(new OrderSpecifier[orderSpecifierList.size()]))
.fetch();
정렬기능 추가
동적 쿼리를 작성하기 위해서는 다양한 조건에 따라 쿼리가 추가 되거나 삭제되는데
where 절은 BooleanBuilder
를 사용하여 동적쿼리를 작성하고
order by 절은 OrderSpecifier
를 사용하여 동적쿼리를 작성한다.
whereBuilder.and(qParkBookingInfo.parkInfo.id.eq(parkInfoId));
switch (state) {
case 1 -> whereBuilder.and(qParkMgtInfo.id.isNull().and(qParkBookingInfo.endTime.gt(LocalDateTime.now())));
case 2 -> whereBuilder.and(qParkMgtInfo.id.isNotNull().and(qParkMgtInfo.exitTime.isNull()));
default -> {
}
}
switch (sort) {
case 0 -> orderSpecifierList.add(qParkBookingInfo.startTime.desc().nullsLast());
case 1 -> orderSpecifierList.add(qParkBookingInfo.endTime.desc().nullsLast());
case 2 -> orderSpecifierList.add(qParkMgtInfo.enterTime.desc().nullsLast());
case 3 -> orderSpecifierList.add(qParkMgtInfo.exitTime.desc().nullsLast());
default -> {
}
}
return jpaQueryFactory.select(Projections.constructor(ParkBookingInfoMgtDto.class,
qParkBookingInfo.carNum, qParkBookingInfo.startTime, qParkBookingInfo.endTime,
qParkBookingInfo.user.id, qParkBookingInfo.exitTime, qParkMgtInfo.parkBookingInfo.id,
qParkMgtInfo.enterTime, qParkMgtInfo.exitTime, qParkMgtInfo.charge))
.from(qParkBookingInfo)
.leftJoin(qParkMgtInfo).on(qParkBookingInfo.id.eq(qParkMgtInfo.parkBookingInfo.id))
.where(whereBuilder)
.orderBy(orderSpecifierList.toArray(new OrderSpecifier[orderSpecifierList.size()]))
.fetch();
페이징 기능 추가
Page객체를 리턴해야하기 때문에 기존에 List로 반환하던 객체를 모두 Page로 변경해줘야 한다
기존에 바로 return 하던 객체를 QueryResults
로 받아 페이징으로 변환해준다
QueryResults<ParkBookingInfoMgtDto> queryReuslt= jpaQueryFactory.select(Projections.constructor(ParkBookingInfoMgtDto.class,
qParkBookingInfo.carNum, qParkBookingInfo.startTime, qParkBookingInfo.endTime,
qParkBookingInfo.user.id, qParkBookingInfo.exitTime, qParkMgtInfo.parkBookingInfo.id,
qParkMgtInfo.enterTime, qParkMgtInfo.exitTime, qParkMgtInfo.charge))
.from(qParkBookingInfo)
.leftJoin(qParkMgtInfo).on(qParkBookingInfo.id.eq(qParkMgtInfo.parkBookingInfo.id))
.where(whereBuilder)
.orderBy(orderSpecifierList.toArray(new OrderSpecifier[orderSpecifierList.size()]))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetchResults();
List<ParkBookingInfoMgtDto> parkBookingInfoMgtDtoList = queryReuslt.getResults();
Long total = queryReuslt.getTotal();
return new PageImpl<>(parkBookingInfoMgtDtoList,pageable,total);
최종코드
@Repository
@RequiredArgsConstructor
public class ParkBookingInfoRepositoryImpl implements ParkBookingInfoRepositoryCustom {
private final JPAQueryFactory jpaQueryFactory;
private final QParkBookingInfo qParkBookingInfo = QParkBookingInfo.parkBookingInfo;
private final QParkMgtInfo qParkMgtInfo = QParkMgtInfo.parkMgtInfo;
@Override
public Page<ParkBookingInfoMgtDto> findByMgtList(Long parkInfoId, int state, int sort, Pageable pageable) {
BooleanBuilder whereBuilder = new BooleanBuilder();
List<OrderSpecifier<?>> orderSpecifierList = new ArrayList<>();
whereBuilder.and(qParkBookingInfo.parkInfo.id.eq(parkInfoId));
switch (state) {
case 1 -> whereBuilder.and(qParkMgtInfo.id.isNull().and(qParkBookingInfo.endTime.gt(LocalDateTime.now())));
case 2 -> whereBuilder.and(qParkMgtInfo.id.isNotNull().and(qParkMgtInfo.exitTime.isNull()));
default -> {
}
}
switch (sort) {
case 0 -> orderSpecifierList.add(qParkBookingInfo.startTime.desc().nullsLast());
case 1 -> orderSpecifierList.add(qParkBookingInfo.endTime.desc().nullsLast());
case 2 -> orderSpecifierList.add(qParkMgtInfo.enterTime.desc().nullsLast());
case 3 -> orderSpecifierList.add(qParkMgtInfo.exitTime.desc().nullsLast());
default -> {
}
}
QueryResults<ParkBookingInfoMgtDto> queryReuslt= jpaQueryFactory.select(Projections.constructor(ParkBookingInfoMgtDto.class,
qParkBookingInfo.carNum, qParkBookingInfo.startTime, qParkBookingInfo.endTime,
qParkBookingInfo.user.id, qParkBookingInfo.exitTime, qParkMgtInfo.parkBookingInfo.id,
qParkMgtInfo.enterTime, qParkMgtInfo.exitTime, qParkMgtInfo.charge))
.from(qParkBookingInfo)
.leftJoin(qParkMgtInfo).on(qParkBookingInfo.id.eq(qParkMgtInfo.parkBookingInfo.id))
.where(whereBuilder)
.orderBy(orderSpecifierList.toArray(new OrderSpecifier[orderSpecifierList.size()]))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetchResults();
List<ParkBookingInfoMgtDto> parkBookingInfoMgtDtoList = queryReuslt.getResults();
Long total = queryReuslt.getTotal();
return new PageImpl<>(parkBookingInfoMgtDtoList,pageable,total);
}
}
개선 결과
모든 코드를 적용 후 동일한 군산오름 주차장의 데이터를 조회했을경우
소요되는 시간은 67ms로 개선되었다.
기존 비효율적인 로직을 QueryDSL 동적쿼리로 개선하였고
기존 1461ms에서 67ms로 약 95.41% 의 성능개선률을 보여주었다.
'공부 > 성능개선' 카테고리의 다른 글
[ParkNav] 코어 로직 동시성제어 테스트 코드 작성 (0) | 2023.04.27 |
---|---|
[ParkNav] QueryDSL을 이용한 관리자 페이지 성능개선 (0) | 2023.04.27 |
[ParkNav] 예약처리 일관성 테스트 (0) | 2023.04.27 |
[ParkNav] 알고리즘 Version 0 ~ Version 2 (0) | 2023.04.27 |
[ParkNav] 검색 성능 개선 기록 (1) | 2023.04.19 |
댓글