해당 포스팅은 사이드 프로젝트 진행 중 겪은 크고 작은 이슈들에 대한 기록입니다.

실제 프로젝트에 적용해보자

  • 현재 진행하고 있는 프로젝트에 Hibernate Spatial을 적용 후, 테스트를 진행해보았다.

도입 목적

  • 기존의 거리 기반 매장 탐색 방식을 개선하고자 Hibernate Spatial을 사용하기로 함

  • 기존보다 성능이 나은지를 기대하고 테스트를 진행

    • 기존 조회 방식

      • Full Table Scan 후, 현재 위치와 매장의 위치 간의 거리를 계산하여 조건에 맞는 데이터 별도 선정
    • 기대한 이유

      • ST_Contains와 같은 포함 관계 함수에서의 공간 인덱스로 R-Tree 알고리즘이 사용되므로 데이터가 늘어날수록 Full Table Scan보다 더 빠를 것이라고 예상함

디테일한 테스트 설정

  • 목적

    • 현재 중심 좌표에서 3km 이내에 존재하는 모든 매장의 정보를 얻고자 함
  • 기댓값

    • 기존의 조회 방식보다 단축된 시간
  • 구분

    1. 기존 조회 방식 (아래 두 방식과 달리 직선 거리를 모두 계산하므로 데이터 수가 더 많을 수 있음)
    2. MBRContains를 활용한 경우
    3. ST_Contains (혹은 ST_Within)을 활용한 경우 (둘은 상반된 기능이며 성능 차이는 없음)
  • 테스트 데이터

    • 총 1천만 111개
    • 101만개, 901만개, 109개 - 3개의 그룹으로 나누어짐. (즉, Point 주소는 총 111개밖에 없음)
      • 109개 (id 범위: 10000000 미만의 숫자 109개)
      • 약 500만개 (id 범위: 10000000 ~ 15000000)
        • 우리 집 기준 3km 이내에 반드시 포함되는 좌표
      • 500만개 (id 범위: 15000001 ~ 20000000)
        • 우리 집 기준 3km 이내에 반드시 포함되지 않는 좌표
      • 중복 데이터로 인한 올바른 테스트 성능이 나올지 파악하고자 함
      • 범위를 정하여 테스트 데이터의 개수를 조정해가며 전체적인 코드 성능을 확인하고자 진행할 예정

사전에 알아야 할 내용

1. MBR이란?

  • Minimum Bounding Rectangle (최소 경계 사각형)
  • 쉽게 얘기하면 주어진 Geometry를 모두 포함할 수 있는 최소 영역의 직사각형이다.
  • MBRContains(g1, g2)
    • g1의 범위에 g2가 포함된다면 1(True)을 반환, 그렇지 않다면 0(False)을 반환
    • 예제에서 g1으로 LineString을 사용했는데, 대각선으로 주어진 선(두 점이 남서, 북동에 있음)의 MBR을 구하면 내가 찾고자 하는 범위가 생성됨
    • 생성한 범위에 g2(Point)가 존재하면 1을 반환함
  • MBRContains의 경우, 공식 문서에서 지원하는 Dialect 메소드에 존재하지 않는데 써보면 정상 작동을 하는데, 왜 동작하는지 정확하게 설명된 정보를 아직까지 찾지 못하였다.

2. 그 밖의 공간 관계 함수

3. JTS, Geolatte 간의 성능 차이

  • 사전에 Geometry로 두 타입을 모두 사용해봤으며 그 결과 유효한 성능 차이는 찾을 수 없었다.
  • 따라서 기능이 좀 더 많은 Geolatte로 결정하였다.

4. 동일한 조건으로 테스트를 수행해도 쿼리 수행 시간이 일정하지는 않음

  • 각각의 테스트 수행 시간은 큰 변화가 없으나 쿼리 수행 시간은 Worst Case인 경우와 Best Case인 경우의 성능 차이가 생각보다 크다.
  • 이를 감안하여 여러번 수행 한 후, 그 결과를 기록하고자 한다.

사용할 메소드들

  • import org.geolatte.geom.G2D;
    import org.geolatte.geom.Geometry;
    import org.locationtech.jts.io.ParseException;
    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.data.jpa.repository.Query;
    import org.springframework.data.repository.query.Param;
    
    import java.util.List;
    
    public interface StoreRepository extends JpaRepository<Store, Long>, StoreRepositoryCustom {
    
        /*
            범위 내의 모든 데이터 조회
         */
        @Query("select s from Store s " +
                "left outer join fetch s.file " +
                "where s.id < :range")
        List<Store> findAllStoresLt(@Param("range") Long range);
    
        /*
            MBCContains
         */
        @Query("select s from Store s " +
                "left outer join fetch s.file " +
                "where mbrcontains(:lineString, s.point) = true and s.id < 20000000")
        List<Store> getStoresByMbrContains(@Param("lineString") Geometry<G2D> lineString) throws ParseException;
    
        /*
            ST_Contains (Polygon)
         */
        @Query("select s from Store s " +
                "left outer join fetch s.file " +
                "where st_contains(:polygon, s.point) = true and s.id < 20000000")
        List<Store> getStoresBySTContains(@Param("polygon") Geometry<G2D> polygon) throws ParseException;
    }
  • 위의 기능들을 사용(JPQL)했으며, Geometry를 만드는 코드는 1부에 있으므로 별도로 첨부하지 않았다.

  • 범위 내 모든 데이터 조회의 경우, 테스트 단에서 별도의 거리 계산 메소드가 따로 존재한다.

  • 위에 조건으로 주어진 store_id의 범위만 조정하면서 테스트를 진행할 예정이다.

  • 조회된 결과의 개수까지 구한 후 테스트 종료.

테스트 결과

조건 1. 조건 범위 내에 100% 포함되는 테스트 약 100만개

전체 테스트 코드 수행 시간

  • case1-1

단순 조회 후 거리 필터 적용

  • case1-2

MBRContains

  • case1-3

ST_Contains

  • case1-4

결론

  • 데이터 100만개 모두가 범위안에 포함되어있기 때문인지 아니면 데이터가 100만개 밖에 되지 않아서 그런건지 쿼리 수행 속도에는 차이가 없다... (오히려 기존 조회 방식이 가장 빨랐음)
  • 전체 수행 시간은 대략 1초 차이가 나는데 찾은 데이터 수가 9개 더 많으므로 유의미한 수치는 아닌듯 하다.

조건 2. 조건 범위 내 포함 50만, 미포함 50만 테스트 총 100만개

전체 테스트 코드 수행 시간

  • case2-1

단순 조회 후 거리 필터 적용

  • case2-2

MBRContains

  • case2-3

ST_Contains

  • case2-4

결론

  • 범위 내 데이터 포함 비율이 50 : 50인 경우, 성능상 이점이 뚜렷하게 나타났다.
  • 테스트 별 로직 수행 시간: 기존 대비 최대 164% 성능 개선율을 보임
  • 쿼리 수행 속도 : 기존 대비 최대 185% 성능 개선율을 보임

조건 3. 조건 범위 내 포함 100만, 미포함 100만 테스트 총 200만개

전체 테스트 코드 수행 시간

  • case3-1

단순 조회 후 거리 필터 적용

  • case3-2

MBRContains

  • case3-3

ST_Contains

  • case3-4

결론

  • 범위 내 데이터 포함 비율이 50 : 50이면서 데이터의 개수를 2배 늘렸다.
  • 테스트 별 로직 수행 시간: 기존 대비 최대 120% 성능 개선율을 보임
  • 쿼리 수행 속도 : 기존 대비 최대 30% 성능 개선율을 보임
  • 조건에 부합하는 데이터의 개수가 많아져 쿼리 수행속도의 이점은 줄어들었으나, 전체 데이터의 개수가 늘어 전체 수행 시간 속도의 이점은 여전히 강력하다.

조건 4. 조건 범위 내 포함 20만, 미포함 80만 테스트 총 100만개

전체 테스트 코드 수행 시간

  • case4-1

단순 조회 후 거리 필터 적용

  • case4-2

MBRContains

  • case4-3

ST_Contains

  • case4-4

결론

  • 범위 내 데이터 포함 비율이 20: 80
  • 테스트 별 로직 수행 시간: 기존 대비 최대 367% 성능 개선율을 보임
  • 쿼리 수행 속도 : 기존 대비 최대 77% 성능 개선율을 보임
  • 이쯤 되니 보이는 것이, 테스트 별 로직 수행 시간은 확실히 주어진 데이터의 조건에 따라 크게 개선되고, 쿼리 수행 속도는 DB와의 연결 상태에 따라 생각보다 큰 오차 범위를 가지는 것 같다.

조건 5. 전체 데이터 조회 (1000만개 - 비율 약 5 : 5)

전체 테스트 코드 수행 시간

  • case5-1

  • Out Of Memory 에러 발생

  • 로컬에서도 에러가 발생하면 이보다 열악한 EC2 환경(가용 메모리 1GB)에서 무조건 OOM이 발생할 것이다.

  • 이렇게 데이터가 많을 땐 사실상 사용할 수가 없는 방법.

  • 잘 보면 쿼리 자체는 수행이 됐다.

  • OOM을 해결하기 위한 방법을 추가적으로 공부해봐야겠다.

최종 결론

  • 기존 조회 방법에 비하여 확실하게 성능이 개선됐다.
  • 테스트 별 로직 수행 시간은 눈에 띄게 개선됐고, 쿼리 수행 속도 역시 Worst Case에서도 성능 개선이 항상 이루어졌으므로 기존 로직을 변경하고 Hibernate Spatial 기능을 사용하는 것으로 결정하였다.

0개의 댓글

관련 채용 정보

Powered by GraphCDN, the GraphQL CDN