Trouble Shooting 기록 04. JPA Hibernate, QueryDSL 그리고 Spatial DB (2)
읽기에 앞서
- 이전 포스팅을 먼저 보고 오시는 걸 추천드립니다!
실제 프로젝트에 적용해보자
- 현재 진행하고 있는 프로젝트에 Hibernate Spatial을 적용 후, 테스트를 진행해보았다.
도입 목적
- 기존의 거리 기반 매장 탐색 방식을 개선하고자 Hibernate Spatial을 사용하기로 함
- 기존보다 성능이 나은지를 기대하고 테스트를 진행
- 기존 조회 방식
- Full Table Scan 후, 현재 위치와 매장의 위치 간의 거리를 계산하여 조건에 맞는 데이터 별도 선정
- 기대한 이유
- ST_Contains와 같은 포함 관계 함수에서의 공간 인덱스로 R-Tree 알고리즘이 사용되므로 데이터가 늘어날수록 Full Table Scan보다 더 빠를 것이라고 예상함
- 기존 조회 방식
디테일한 테스트 설정
- 목적
- 현재 중심 좌표에서 3km 이내에 존재하는 모든 매장의 정보를 얻고자 함
- 기댓값
- 기존의 조회 방식보다 단축된 시간
- 구분
- 기존 조회 방식 (아래 두 방식과 달리 직선 거리를 모두 계산하므로 데이터 수가 더 많을 수 있음)
- MBRContains를 활용한 경우
- 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만개
전체 테스트 코드 수행 시간
단순 조회 후 거리 필터 적용
MBRContains
ST_Contains
결론
- 데이터 100만개 모두가 범위안에 포함되어있기 때문인지 아니면 데이터가 100만개 밖에 되지 않아서 그런건지 쿼리 수행 속도에는 차이가 없다… (오히려 기존 조회 방식이 가장 빨랐음)
- 전체 수행 시간은 대략 1초 차이가 나는데 찾은 데이터 수가 9개 더 많으므로 유의미한 수치는 아닌듯 하다.
조건 2. 조건 범위 내 포함 50만, 미포함 50만 테스트 총 100만개
전체 테스트 코드 수행 시간
단순 조회 후 거리 필터 적용
MBRContains
ST_Contains
결론
- 범위 내 데이터 포함 비율이 50 : 50인 경우, 성능상 이점이 뚜렷하게 나타났다.
- 테스트 별 로직 수행 시간: 기존 대비 최대 164% 성능 개선율을 보임
- 쿼리 수행 속도 : 기존 대비 최대 185% 성능 개선율을 보임
조건 3. 조건 범위 내 포함 100만, 미포함 100만 테스트 총 200만개
전체 테스트 코드 수행 시간
단순 조회 후 거리 필터 적용
MBRContains
ST_Contains
결론
- 범위 내 데이터 포함 비율이 50 : 50이면서 데이터의 개수를 2배 늘렸다.
- 테스트 별 로직 수행 시간: 기존 대비 최대 120% 성능 개선율을 보임
- 쿼리 수행 속도 : 기존 대비 최대 30% 성능 개선율을 보임
- 조건에 부합하는 데이터의 개수가 많아져 쿼리 수행속도의 이점은 줄어들었으나, 전체 데이터의 개수가 늘어 전체 수행 시간 속도의 이점은 여전히 강력하다.
조건 4. 조건 범위 내 포함 20만, 미포함 80만 테스트 총 100만개
전체 테스트 코드 수행 시간
단순 조회 후 거리 필터 적용
MBRContains
ST_Contains
결론
- 범위 내 데이터 포함 비율이 20: 80
- 테스트 별 로직 수행 시간: 기존 대비 최대 367% 성능 개선율을 보임
- 쿼리 수행 속도 : 기존 대비 최대 77% 성능 개선율을 보임
- 이쯤 되니 보이는 것이, 테스트 별 로직 수행 시간은 확실히 주어진 데이터의 조건에 따라 크게 개선되고, 쿼리 수행 속도는 DB와의 연결 상태에 따라 생각보다 큰 오차 범위를 가지는 것 같다.
조건 5. 전체 데이터 조회 (1000만개 - 비율 약 5 : 5)
전체 테스트 코드 수행 시간
-
Out Of Memory 에러 발생
-
로컬에서도 에러가 발생하면 이보다 열악한 EC2 환경(가용 메모리 1GB)에서 무조건 OOM이 발생할 것이다.
-
이렇게 데이터가 많을 땐 사실상 사용할 수가 없는 방법.
-
잘 보면 쿼리 자체는 수행이 됐다.
- OOM을 해결하기 위한 방법을 추가적으로 공부해봐야겠다.
최종 결론
- 기존 조회 방법에 비하여 확실하게 성능이 개선됐다.
- 테스트 별 로직 수행 시간은 눈에 띄게 개선됐고, 쿼리 수행 속도 역시 Worst Case에서도 성능 개선이 항상 이루어졌으므로 기존 로직을 변경하고 Hibernate Spatial 기능을 사용하는 것으로 결정하였다.
댓글남기기