[ParkNav] 검색 성능 개선 기록

Jae Hun Lee·2023년 4월 19일
0

parknav

목록 보기
7/8
post-thumbnail

성능 개선 결과

최초검색 : 13724 ms
조회로직 변경 : 7847ms
Fetch Join 변경 : 319ms
조회 쿼리 변경 : 256ms

기본 검색 기능

기본적인 주차장 검색기능을 구현했다.

검색어로 검색

  • 카카오맵 API로 사용자 검색어에 대한 결과를 호출
    • 검색 결과가 있는경우 : 검색결과 0번으로 좌표를 반환
    • 검색 결과가 없는경우 : 초기위치 ( 서울역 ) 반환
  • 좌표 주변의 주차장 검색
    • 검색 결과가 있는경우 : 검색 결과를 반환
    • 검색 결과가 없는경우 : 초기위치 ( 서울역 ) 반환

현위치로 검색

  • 현위치 정보 동의 여부
    • 동의 : 현위치 좌표 주변 DB검색
    • 비동의 : 검색실패 위치동의 알림 창 출력
  • 검색결과가 있는지 여부
    • 검색 결과가 있는 경우 : 주변 주차장 리스트 반환
    • 검색 결과가 없는 경우 : 초기위치 ( 서울역 ) 반환

키워드와 일치하는 결과 반환

  • API에서 정확히 검색어가 일치 하는경우 해당 좌표를 반환하게 변경
  • 해당 좌표 주변의 주차장 결과만 반환을 하다보니 검색 중심 위치가 어디인지 확인이 되지않음
    • 검색 중심 위치의 좌표를 같이 반환하여 해당 문제 해결

검색어로 검색

  • 카카오맵 API로 사용자 검색어에 대한 결과를 호출
    • 검색 결과가 있는경우 : 검색결과 0번으로 좌표를 반환
      • 주차장 명이 정확히 일치한다면 해당 좌표 반환
    • 검색 결과가 없는경우 : 초기위치 ( 서울역 ) 반환
  • 좌표 주변의 주차장 검색
    • 검색 결과가 있는경우 : 검색 결과를 반환 + 검색 위치 좌표 반환
    • 검색 결과가 없는경우 : 초기위치 ( 서울역 ) 반환

현위치로 검색

  • 현위치 정보 동의 여부
    • 동의 : 현위치 좌표 주변 DB검색
    • 비동의 : 검색실패 위치동의 알림 창 출력
  • 검색결과가 있는지 여부
    • 검색 결과가 있는 경우 : 주변 주차장 리스트 반환 + 검색 위치 좌표 반환
    • 검색 결과가 없는 경우 : 초기위치 ( 서울역 ) 반환

키워드 유사도 도입

문제점

  • 카카오 API에 검색한 결과만 검색이 가능하다
  • DB에 저장되어있는 주차장명은 검색이 불가능하다
  • 주차장명으로 검색 할 경우 엉뚱한 결과를 반환하기도 한다
  • 문제점
    • 아래와 같이 서귀포 스타벅스를 검색

  • 검색해서 나온 결과인 스타벅스 서귀포DT 주차장을 재검색

  • 엉뚱한 결과인 스타벅스 제주성산DT점 주차장이 나오게됨
  • 스타벅스 서귀포 DT주차장으로 검색했을때 0번째 리턴받은 JSON
    {
    	"address_name":"제주특별자치도 서귀포시 성산읍 고성리 238",
    	"category_group_code":"PK6",
    	"category_group_name":"주차장",
    	"category_name":"교통,수송 \u003e 교통시설 \u003e 주차장",
    	"distance":"",
    	"id":"1648733158",
    	"phone":"",
    	"place_name":"스타벅스 제주성산DT점 주차장",
    	"place_url":"http://place.map.kakao.com/1648733158",
    	"road_address_name":"",
    	"x":"126.920615213766",
    	"y":"33.4497082943452"
    }

해결방법

  • 검색어 유사도를 도입해 유사도가 가장 높은 항목의 좌표를 반환

  • 키워드 검색 시 기존에 카카오 API만 검색하던 부분을 DB의 주차장 명칭도 포함

  • 해결

    • 코드에 키워드 유사도 로직 추가

      if (parkSearchRequestDto.getKeyword() != null) {
          //카카오 검색 API호출
          KakaoSearchDto kakaoSearchDto = kakaoMapService.getKakaoSearch(parkSearchRequestDto.getKeyword());
          if (kakaoSearchDto.getMeta().getTotal_count() > 0) {
              List<KakaoSearchDocumentsDto> kakaoSearchDocumentsDto = kakaoSearchDto.getDocuments();
              lo = kakaoSearchDocumentsDto.get(0).getX();
              la = kakaoSearchDocumentsDto.get(0).getY();
              placeName = kakaoSearchDocumentsDto.get(0).getPlace_name();
              // 0번 결과로 유사도 초기화
              similarScore = similarKeyword(parkSearchRequestDto.getKeyword(), kakaoSearchDocumentsDto.get(0).getPlace_name());
              for (KakaoSearchDocumentsDto kakao : kakaoSearchDocumentsDto) {
                  // 검색결과 별 유사도 비교
                  searchSimilarScore = similarKeyword(parkSearchRequestDto.getKeyword(), kakao.getPlace_name());
                  if (searchSimilarScore < similarScore) {
                      lo = kakao.getX();
                      la = kakao.getY();
                      placeName = kakao.getPlace_name();
                      similarScore = searchSimilarScore;
                  }
              }
          }
          // DB에서 Like검색
          List<ParkInfo> parkInfos = parkInfoRepository.findByNameContains(parkSearchRequestDto.getKeyword());
          for (ParkInfo parkInfo : parkInfos) {
              // 검색결과 별 유사도 비교
              searchSimilarScore = similarKeyword(parkSearchRequestDto.getKeyword(), parkInfo.getName());
              if (searchSimilarScore < similarScore) {
                  la = parkInfo.getLa();
                  lo = parkInfo.getLo();
                  placeName = parkInfo.getName();
                  similarScore = searchSimilarScore;
              }
          }
          //결과가 없을경우 리턴
          if (placeName == null) {
              return parkSearchResponseDto;
          }
      
      public static int similarKeyword(String userKeyword, String keyword) {
          return LevenshteinDistance.getDefaultInstance().apply(userKeyword, keyword);
      }
    • 스타벅스 서귀포 검색 시 결과

    • 스타벅스 서귀포DT 주차장 검색 결과

    • 사용자 화면에서도 정상적으로 확인이 된다

검색 시간이 오래 걸리는 이슈

  • 기존에는 검색 속도가 매우 빨랐으나 서비스 로직을 추가하며 어느순간부터 오랜시간이 걸림
  • 서울특별시청을 검색할경우 검색 결과가 약 13초 후에 출력이 됨

문제점

  • 검색 시 수많은 쿼리가 발생됨 ( 검색지역에 주차장이 많을수록 비례 )
  • 많은 쿼리가 발생되는 지점을 분석해본 결과 새롭게 추가 된 현재 예약가능 구획 수 기능에서 수많은 쿼리를 발생시킴
    result = parkInfoRepository.findParkInfoWithOperInfoAndTypeQueryDsl(lo, la, 2, ParkType.fromValue(parkSearchRequestDto.getType()));
            for (ParkOperInfo park : result) {
                String available;
                // 현재 운영여부 확인
                if (OperationChecking.checkOperation(LocalDateTime.now(), park)) {
                    // 현재 주차 가능 대수 = 주차 가능 대수 - 출차시간이 없는 현황 수(주차중인 경우)
                    available = (park.getCmprtCo() - parkMgtInfoRepository.countByParkInfoIdAndExitTimeIsNull(park.getParkInfo().getId())) + "대";
                } else {
                    // 운영중이 아니라면 메시지 출력
                    available = MsgType.NOT_OPEN_NOW.getMsg();
                }
    
                ParkOperInfoDto parkOperInfoDto = ParkOperInfoDto.of(park, ParkingFeeCalculator.calculateParkingFee(parkSearchRequestDto.getParktime() * 60L, park), available);
                if (parkOperInfoDto.getTotCharge() <= parkSearchRequestDto.getCharge()) {
                    parkOperInfoDtos.add(parkOperInfoDto);
                }
            }
  • 주변 주차장을 조회하는 쿼리에서 Join이 되어있어 주차장 갯수에 비례한 쿼리가 생성됨
    @Override
        public List<ParkOperInfo> findParkInfoWithOperInfoAndTypeQueryDsl(String x, String y, double distance, String type) {
            Double longitude = Double.parseDouble(x);
            Double latitude = Double.parseDouble(y);
            BooleanBuilder builder = new BooleanBuilder();
            builder.and(Expressions.booleanTemplate("ST_Distance_Sphere(Point({0}, {1}), Point({2}, {3})) < {4}", longitude, latitude, qParkInfo.lo, qParkInfo.la, 2000));
    
            if (!type.equals("전체")) {
                builder.and(qParkOperInfo.parkCtgy.eq(type));
            }
    
            return jpaQueryFactory.selectFrom(qParkOperInfo)
                    .join(qParkOperInfo.parkInfo, qParkInfo)
                    .where(builder)
                    .orderBy(Expressions.stringTemplate("ST_Distance_Sphere(Point({0}, {1}), Point({2}, {3}))", longitude, latitude, qParkInfo.lo, qParkInfo.la).asc())
                    .fetch();
        }

해결

  • 현재 예약가능 구획 수
    • 로직을 따로 API를 분리하여 주차장을 눌렀을때 호출하게 변경하여 사용자가 확인하는 주차장만 조회하게 변경
    • 적용 후 약 7.8초 소요되게 변경

  • 주변 주차장 조회 쿼리
    • 기존로직에 Fetch Join을 적용하여 여러번 조회하던 부분을 한번에 확인가능하게 변경
    • 적용 후 약 0.3초 소요되게 변경

  • 추가 작업

    • 기존 쿼리는 ST_Distance_Sphere를 이용하여 거리를 계산했는데 176ms가 소요되었다

    • 거리 계산 공식을 적용하여 측정했을때는 84ms로 50%정도 더 빠른 효율을 보여 QueryDSL에 해당 쿼리를 적용시켰다

결과

  • 변경 전 서울특별시청을 검색했을때 소요 시간 : 13724ms
  • 변경 후 서울특별시청을 검색했을때 소요 시간 : 256ms
  • 성능 개선률 : 98.13%

profile
기록을 남깁니다

0개의 댓글