읽기에 앞서

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

  • 현재 진행하고 있는 프로젝트에 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 기능을 사용하는 것으로 결정하였다.

댓글남기기