해당 포스팅은 사이드 프로젝트 진행 중 겪은 크고 작은 이슈들에 대한 기록입니다.
현재 범위로 잡은 Polygon의 문제점
- 현재 기준 Point에서 부터 Nkm 떨어진 공간을 표현하는 Geometry로 Polygon을 사용하고 있는데, 이는 대략 아래와 같은 그림이다.
- 빨간점이 기준 Point라고 할 때, Point로부터 Nkm 떨어진 [북서 북동 남동 남서] 좌표를 찾아 이를 기준으로 정사각형 모양의 Polygon을 생성하고 있는 것을 알 수 있다.
- 이제 우린 저 범위 내의 좌표점들을 얻을 수 있게 되는 것인데, 곰곰히 생각해보니 여기엔 문제점이 있다. 과연 우리가 얻고자 하는 Nkm의 모든 범위 이내의 모든 좌표를 얻을 수 있는 것일까?
- 너무나 당연한 얘기겠지만 직각이등변삼각형에서 대각선의 길이는 두 밑변의 길이보다 무조건 길다. 따라서 같은 거리 Nkm에 위치한 파란점은 만들어진 Polygon의 내부에 포함되지 않아서 조건에 부합하지 않고 결국 Select 쿼리의 결괏값에 포함되지 않는다.
- 따라서 아래와 같이 빨간점에서부터 4개의 기준 좌표들까지의 거리를 Nkm가 아닌 N√2로 잡고 Polygon을 생성한 후, 그 범위 내에서 Nkm안에 포함되는 좌표들을 구해야 한다. 즉, r=N인 원의 범위가 최종적으로 우리가 찾고자 하는 공간 데이터의 탐색 범위가 될 것이다. (피타고라스의 정리)
어떻게 원을 표현할 것인가?
- 결국 정확한 거리를 기반으로 탐색을 수행하려면 탐색 범위를 원으로 만들어 사용하는 방법을 찾아야했고 현재까지 할 수 있는 방법을 총 2가지 찾았다.
1. org.locationtech.jts의 GeometricShapeFactory 활용
2. ST_Distance를 함께 사용
- N√2로 4개 좌표를 구하여 이를 첫번째 탐색 범위로 잡고 (ST_Contains), 앞서 생성한 범위 중ST_Distance를 추가 조건으로 활용하여 Nkm 이내인 좌표들만 탐색한다.
- QueryDSL 활용 예시
public List<Store> getStoresByStContains(Geometry<G2D> polygon) {
return queryFactory
.select(store)
.from(store)
.leftJoin(store.file, uploadFile)
.fetchJoin()
.where(stContains(polygon), stDistance(polygon).loe(3))
.fetch();
}
private BooleanExpression stContains(Geometry<G2D> polygon) {
return GeometryExpressions
.asGeometry(polygon)
.contains(store.point);
}
private NumberExpression<Double> stDistance(Geometry<G2D> polygon) {
return GeometryExpressions
.asGeometry(store.point)
.distance(polygon);
}
Geolatte에서 원을 생성하는 방법은?
- JTS의 GeometricShapeFactory처럼 Geolatte에도 비슷한 기능이 있을까 싶어 찾아보았으나 찾지 못하였다. 있을 것 같긴 한데 생각보다 내용이 복잡하여 보류하였다.
- 비슷하게 org.geolatte.geom.cga 패키지 내부에 보면 Circle이라는 클래스가 있다. 이를 활용하면 Circle 객체를 만들 수는 있으나 얘를 어디에 사용할 수 있는지 찾지 못하였다... Circle 클래스는 Geometry를 상속받은 클래스가 아닌 독립적인 Class여서 (그냥 부모 클래스 자체가 하나도 없음) Geometry를 사용하는 메소드에 사용할 수도 없고, qureydsl-spatial 내부를 검색해봐도 Circle 객체가 사용되는 곳은 CircularArcLinearizer라는 클래스 밖에 없는데 이마저도 Circle 객체를 선형화 시키는 기능을 할 뿐이고 이를 Geometry로 사용하는 방법은 찾지 못하였다.
- 아직 Spatial에 관하여 공부가 부족하다는 것을 느낀다.
어떤 방법을 사용할까?
- 결론부터 말하자면 1번이 아닌 2번 방식을 사용하는게 좋은 것 같다고 생각한다.
1. 두가지 라이브러리에 종속적이지 않아도 된다.
- GeometricShapeFactory 기능은 Geolatte의 기능이 아니다. 결국 JTS와 Geolatte 두 라이브러리 모두에 종속적인 기능이 만들어지게 되는데 개인적으로 난 대안이 있거나 성능상 확실한 이점이 없다면 여러가지 라이브러리를 쓰는 것을 선호하지 않는다.
2. 원하는 거리 기준이 뭔지 모르겠다.
private org.locationtech.jts.geom.Geometry createCircle(double x, double y, double radius) {
GeometricShapeFactory factory = new GeometricShapeFactory();
factory.setNumPoints(32);
factory.setCentre(new Coordinate(x, y));
factory.setSize(radius * 2);
return factory.createCircle();
}
- setNumPoints는 설명이라도 친절해서 활용은 못했지만 용도가 뭔지는 알 수 있다.
Sets the total number of points in the created {@link Geometry}. The created geometry will have no more than this number of points, unless more are needed to create a valid geometry.
- 하지만 정작 중요한 거리 기준인 반지름(radius)의 값이 어떤 단위로 작동하는지 알 수 없어 정확한 거리 계산을 할 수가 없다.
- 실제 테스트에서도 어림짐작하여 값을 넣어서 사용했을 뿐 아직까지 정확한 기준 단위를 찾지 못하였다.
3. 심지어 2번이 더 빠르다.
- 몇번을 실행해봐도 쿼리 실행 속도도 더 빠르고 전체 로직 수행 시간 역시 더 빠르다.
- GeometryShapeFactory로 Circle을 생성하는 시간이 생각보다 좀 걸리는 것 같다.
- 전체 로직 수행 속도 비교
마무리
- 최종적으로 Geolatte + QueryDSL Spatial + (ST_Contains + ST_Distance)를 사용하여 공간 데이터를 다루기로 결정하였습니다.
- 제가 처음 Spatial DB를 사용하기로 마음 먹고 공부를 시작하였을 때 많이 막막하고 자료도 없어서 힘들었기에 저와 같은 초보이시거나 새로이 도입해보고자 하시는 분이 이 포스팅들을 보시면서 Spatial DB의 첫걸음 용 나침반으로나마 사용될 수 있었으면 좋겠습니다.
- 아직 많이 미숙하고 저 또한 초보이기에 틀린 정보가 있거나 더 좋은 방법이 있다면 댓글 달아주시면 감사하겠습니다!