이번 스프린트에서 전시관
을 저장해서 정보로 제공하게 되었는데,
사용자의 위치에 기반해 근처 전시관을 필터링하여 제공해야 했다.
Google Places API에서 해당 공간의 위도와 경도 값을 자세하게 제공하기 때문에
이를 DB에 저장하고, 필터링에 활용하기로 결정했다.
현재 프로젝트에서 사용하는 MySQL에서는 Spatial Data Type을 지원한다.
즉, 위도와 경도를 가지는 하나의 데이터 타입을 컬럼으로 사용할 수 있다.
(https://dev.mysql.com/doc/refman/8.4/en/spatial-type-overview.html)
그리고 당연히 그 데이터 타입을 바탕으로 연산도 가능하다.
우리는 st_buffer
와 st_contains
를 활용하여 요구사항을 해결할 것이다.
dependencies {
...
// GEOLOGY
implementation 'org.hibernate.orm:hibernate-spatial:6.0.0.Final'
implementation 'org.locationtech.jts:jts-core:1.18.1'
implementation 'com.querydsl:querydsl-sql-spatial:4.1.4'
}
우선 build.gradle
에 필요한 의존 라이브러리를 추가해준다.
위 2개는 JPA를 위해, 아래 1개는 QueryDsl을 위해 선언했다.
import org.locationtech.jts.geom.Point;
public class Location {
private Long id;
private Point geometry;
...
}
그리고 위치 값을 포함하는 엔티티에
Point 타입의 필드를 선언한다.
@Configuration
public class GeometryConfig {
private static final int WGS84_SRID = 4326;
@Bean
public GeometryFactory geometryFactory() {
return new GeometryFactory(new PrecisionModel(), WGS84_SRID);
}
}
본격적으로 위치 인스턴스를 생성하기 전에
위치 인스턴스를 생성해주는 GeometryFactory
를 빈으로 등록해준다.
WGS84_SRID
는 WGS84라는 표준 좌표계의 Spatial Reference ID를 뜻한다.
PrecisionModel은 기본적인 것을 그대로 사용했다.
BooleanExpression isNear = JTSGeometryExpressions
.asJTSGeometry(currentLocation)
.buffer(distance)
.contains(placeEntity.location.geometry)
QueryDsl이 위치 연산을 지원하기 때문에
위와 같이 Point 타입의 currentLocation
을 변환하고,
기준 거리인 distance
로 버퍼 연산하고
contains
로 내가 정의한 geometry 컬럼을 연산할 수 있다.
@Test
void 특정_거리_안에_있는_장소_리스트를_전시_시작일의_역순으로_조회할_수_있다() {
// given
double distance = 4000;
Place samePlace = placeRepository.save(PlaceFixture.create(LocationFixture.create(geometryFactory.createPoint(new Coordinate(0, 0)))));
Place closePlace = placeRepository.save(PlaceFixture.create(LocationFixture.create(geometryFactory.createPoint(new Coordinate(0.01, 0.01)))));
Place farPlace = placeRepository.save(PlaceFixture.create(LocationFixture.create(geometryFactory.createPoint(new Coordinate(1, 1)))));
// when
Slice<Place> result = placeRepository.findAllByLocationOrderByExhibitionStartDateDesc(
PageRequest.ofSize(10),
geometryFactory.createPoint(new Coordinate(0, 0)),
distance
);
// then
assertAll(
() -> assertThat(result.getContent().size()).isEqualTo(2),
() -> assertThat(result.getContent().get(0).getId()).isEqualTo(samePlace.getId()),
() -> assertThat(result.getContent().get(1).getId()).isEqualTo(closePlace.getId())
);
}
위와 같이 테스트를 진행했다.
4000m = 4km는 (0, 0) 기준으로 (0.0356, 0)과의 거리라고 계산했기 때문에
(0, 0)이나 (0.01, 0.01)은 조회에 걸리지만, (1, 1)은 걸리지 않는다.
보통 우리가 좌표를 위도, 경도 순으로 인식하지만
위의 asJTSGeometry()
는 경도, 위도 순으로 인식하기 때문에 반대로 넣어줘야 한다.
테스트 실행 시에는 위와 같은 쿼리 로그를 확인할 수 있었다.
아직 실 서비스에 적용하지는 않았기 때문에
좌표를 DB에서 계산함으로써 가지는 트레이드오프에 대해서는 미리 알지 못한다.
그렇지만 애플리케이션에서의 좌표 연산에 대한 부담을 덜 수 있었고,
우선은 구현했다는 점에 먼저 만족하고 싶다.
🥹🥹🥹