개선 전: 1461 ms
동적 쿼리 적용 : 67ms
기존 관리자 페이지를 호출 시 페이지를 호출하는 시간이 너무 오래걸리는 문제가 발생했고
페이지 호출 시간 = 데이터의 개수 만큼의 페이지 지연이 발생되었다.
군산오름 주차장 기준 1461ms 가 소요되었다.
기존 로직의 흐름에 문제가있었는데
다른 두 테이블의 데이터를 합쳐야 하고 검색조건에 맞는 결과만 내어줘야하기 때문에
두번의 DB호출에서 Pageable을 걸지 못했고
얻어온 데이터를 합치는 과정에서 검색조건을 필터링 했는데
그 결과 많은 쿼리 조회로 인해 페이지 조회가 느려진것이다.
기존 로직은 다음과 같다.
// 예약시간이 종료되지 않은 예약 정보를 불러오기
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을 사용하면 동적쿼리를 생성하기 좋기때문에 사용하게 되었다.
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
은 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% 의 성능개선률을 보여주었다.