인식한 상황
기존에는 walking_loads와 locations 테이블이 1대 N 관계로 구성되어 있었다. 산책로에 대응하는 location 수가 많아질 경우, 특정 지역(예: 현재 위치) 주변 1km 내 산책로를 조회하는 로직에서 다음과 같은 문제가 발생했다.
과도한 데이터 로드: 모든 산책로를 조회한 뒤 자바 코드상에서 거리 계산을 수행함으로써, 불필요한 엔티티를 대량으로 불러오는 비효율이 있었다.
성능 저하: 1000개의 더미 데이터 기준으로 API 응답 시간이 약 2초에 달할 정도로 비효율적이었다.
해결 과정
이런 문제들을 해결하기 위해 공간형 데이터를 고려하게 되었고, MySQL에서 제공하는 Spatial 기능을 활용하기로 하였다.
JPA와 공간형 데이터 활용 이슈 극복
MySQL의 Multi Point 타입을 이용해 산책로 위치를 효율적으로 관리하고자 했으나, JPA는 기본적으로 Point 형은 지원하나 Multi Point 형은 지원하지 않는다는 점이 문제가 되었다. 이를 해결하기 위해 hibernate-spatial 라이브러리를 도입했다. 위·경도 데이터를 Coordinate로 변환한 뒤 Geometry Factory를 사용하여 Point 객체를 생성한다. 이로써 Multi Point를 JPA 상에서 매핑할 수 있게 되어, DB 상의 공간형 컬럼과 엔티티 간 매핑 문제를 해결했다.
거리 계산 로직 DB 레벨로 이전
MySQL이 제공하는 ST_Distance_Sphere 함수를 통해 특정 좌표로부터 반경 1km 내의 데이터만 필터링하려고 했다. 이를 통해 불필요한 엔티티 로드를 줄일 수 있었고, 초기 성능 개선을 이뤄 약 2초에서 200ms대로 단축하는 효과를 보았다.
기존에는 약 200줄에 달하는 자바 로직에서 거리 계산을 수행했다. 하지만 위를 DB 레벨로 옮겨서 성능을 개선하였다.
결과
초기 개선만으로 약 2초에서 200ms로 단축되었다. 추가로 ST_Buffer와 ST_Contains를 통해 인덱스를 활용한다면, 더 많은 데이터 규모에서도 안정적인 성능을 유지할 수 있다. 기존 200줄에 달했던 로직을 DB 레벨의 GIS 함수 활용으로 단축, 유지보수성을 향상했다.
하지만 추가로 검색해 보니 ST_Distance_Sphere 함수는 Spatial Index를 지원하지 않고 Full Scan을 한다고 한다… 시간이 없어서 해보지는 못했지만 같은 방법을 사용할 수 있을 거 같다는 생각이 들었다. 만약 10만개의 데이터라면 더 유효한 결과가 나오지 않을까? 지금도 사실상 90% 정도 속도를 올릴 수 있었는데, 아래 방법을 추가로 적용하면 더 빨라지지 않을까 의견을 내본다…
ST_Buffer:
주어진 좌표(Point)와 반지름을 사용해 원형(Polygon) 영역을 생성한다.
예:ST_Buffer(ST_GeomFromText(:standard_point,4326), 5000)
이 로직은 주어진 좌표를 중심으로 반경 1km의 원형 영역(Polygon)을 만든다.ST_Contains:
ST_Contains 함수를 통해 ST_Buffer로 생성한 Polygon 내부에 위치한 Point를 가진 Walking Load 엔티티만 필터링할 수 있다.
예:ST_CONTAINS((ST_Buffer(ST_GeomFromText(:standard_point,4326), 5000)), walking_load_point)
이를 통해 Polygon 영역 내에 포함되는 산책로만 조회하게 된다.
주의사항 해당 방법을 사용하기 위해서는 SRID가 Column에 적용 되어 있어야 한다!!