[ParkNav] QueryDSL을 이용한 관리자 페이지 성능개선

Jae Hun Lee·2023년 4월 27일
0

parknav

목록 보기
2/8
post-thumbnail

성능 개선 결과

개선 전: 1461 ms
동적 쿼리 적용 : 67ms

문제점

기존 관리자 페이지를 호출 시 페이지를 호출하는 시간이 너무 오래걸리는 문제가 발생했고

페이지 호출 시간 = 데이터의 개수 만큼의 페이지 지연이 발생되었다.

군산오름 주차장 기준 1461ms 가 소요되었다.

원인

기존 로직의 흐름에 문제가있었는데

다른 두 테이블의 데이터를 합쳐야 하고 검색조건에 맞는 결과만 내어줘야하기 때문에

두번의 DB호출에서 Pageable을 걸지 못했고

얻어온 데이터를 합치는 과정에서 검색조건을 필터링 했는데

그 결과 많은 쿼리 조회로 인해 페이지 조회가 느려진것이다.

기존 로직은 다음과 같다.

  1. 주차장의 모든 예약정보를 가져온다.
  2. 예약 정보 중 입차 한 기록이 있는지 DB에서 검색한다.
    1. 입차 했을경우 : 검색 조건에 맞는 데이터를 입차 차량으로 리스트에 추가한다.
    2. 입차 안했을경우 : 검색 조건에 맞는 데이터를 미 입차 차량으로 리스트에 추가한다.
  3. 리스트에 추가 된 차량을 정렬 조건에 맞춰 정렬한다.
  4. 리스트를 페이징화 시켜 리스트를 리턴한다.
  • 기존 코드
    // 예약시간이 종료되지 않은 예약 정보를 불러오기
    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% 의 성능개선률을 보여주었다.

profile
기록을 남깁니다

0개의 댓글