해당 포스팅은 사이드 프로젝트 진행 중 겪은 크고 작은 이슈들에 대한 기록입니다.
Spatial DB, 우리 말로 공간 DB란 공간 데이터를 다루기 위한 특수 목적으로 사용되는 DB
목적
흔히 접할 수 있는 RDBMS에 공간 데이터를 다루기 위한 기능들이 탑재되어 있음.
출처 : 참고 블로그
자주 사용되는 공간 데이터 타입
공간 데이터 타입 | 정의 | SQL 예 |
---|---|---|
Point | 좌표 공간 한 지점의 위치 (경도, 위도 순서로 입력) | POINT(10 10) |
LineString | 다수의 Point를 연결해주는 선분 | LINESTRING(10 10, 20 20, 30 30) |
Polygon | 다수의 선분들이 연결되어 닫혀 있는 다각형 각각의 Point의 처음과 끝은 같은 좌표를 공유해야 하며 Polygon 전체에서 처음과 끝은 같은 Point로 이루어져야 함 | POLYGON((10 10, 10 20, 20 20, 20, 10, 10 10)) |
Multi-Point | 다수의 Point 집합 | MULTIPOINT(10 10, 30 20) |
Multi-LineString | 다수의 LineString 집합 | MULTILINESTRING((10 10, 20 20), (20 15, 30 40)) |
Mulit-Polygon | 다수의 Polygon 집합 | MULTIPOLYGON ((( 10 10, 15 10, 20 15, 20 25, 15 20, 10 10 )) , (( 40 25, 50 40, 35 35, 25 10, 40 25 )) ) |
GeomCollection | 모든 공간 데이터들의 집합 | GEOMETRYCOLLECTION ( POINT (10 10), LINESTRING (20 20, 30 40), POINT (30 15) ) |
출처 : 참고 블로그
두 공간 객체 간의 관계를 일반 데이터 타입으로 반환해주는 함수 (Boolean 또는 Number)
MySQL에서 제공해주는 공간 관계 함수 중 자주 사용되는 함수들
공간 관계 함수 | 설명 |
---|---|
ST_Equals (g1 Geometry, g2 Geometry) : Boolean | g1과 g2가 동일하면 True를 반환하고 상이하다면 False를 반환 |
ST_Disjoint (g1 Geometry, g2 Geometry) : Boolean | g1과 g2가 겹치는 곳 없다면 True를 반환하고, 겹치는 곳이 있으면 False를 반환 |
ST_Within (g1 Geometry, g2 Geometry) : Boolean | g1가 g2 영역 안에 포함된 경우 True를 반환하고 그렇지 않은 경우 False를 반환 (Contains와 반대) |
ST_Overlaps (g1 Geometry, g2 Geometry) : Boolean | g1과 g2 영역 중 교집합 영역이 존재하는 경우 True를 반환하고 존재하지 않는 경우 False를 반환 |
ST_Intersects (g1 Geometry, g2 Geometry) : Boolean | g1과 g2 영역 간에 교집합이 존재하는 경우 True를 반환하고 그렇지 않은 경우 False를 반환 |
ST_Contains (g1 Geometry, g2 Geometry) : Boolean | g2가 g1 영역 안에 포함된 경우 True를 반환하고 그렇지 않은 경우 False를 반환 (Within과 반대) |
ST_Touches (g1 Geometry, g2 Geometry) : Boolean | g1과 g2가 경계 영역에서만 겹치는 경우 결과 값으로 True를 반환하며 경계 영역 외에서 겹치거나 겹치는 곳이 없다면 False를 반환 |
ST_Distance (g1 Geometry, g2 Geometry) : Double | g1과 g2간의 거리를 반환 |
출처 - 참고 블로그
두 공간 객체의 연산 결과를 새로운 공간 객체로 반환해주는 함수
MySQL에서 제공해주는 공간 연산 함수 중 자주 사용되는 함수들
공간 연산 함수 | 설명 |
---|---|
ST_Intersection (g1 Geometry, g2 Geometry) : Geometry | g1과 g2의 교집합인 공간 객체를 반환 |
ST_Union (g1 Geometry, g2 Geometry) : Geometry | g1과 g2의 합집합인 공간 객체를 반환 |
ST_Difference (g1 Geometry, g2 Geometry) : Geometry | g1과 g2의 차집합인 공간 객체를 반환 |
ST_Buffer (g1 Geometry, d Double ) : Geometry | g1에서 d 거리만큼 확장된 공간 객체를 반환 |
ST_Envelope (g1 Geometry) : Polygon | g1을 포함하는 최소 MBR인 Polygon을 반환 |
ST_StartPoint (l1 LineString) : Point | l1의 첫 번째 Point를 반환 |
ST_EndPoint (l1 LineString) : Point | l1의 마지막 Point를 반환 |
ST_PointN (l1 LineString) : Point | l1의 n 번째 Point를 반환 |
Hibernate에서 Spatial Data를 사용하려면 org.hibernate:hibernate-core 의존성 외에 아래처럼 별도의 의존성, org.hiberate:hibernate-spatial을 추가해줘야만 한다.
공식 문서에 따르면 Hibernate 5.0에 들어서야 정식 Hibernate ORM Project로 소속되었다고 한다.
아래 트리와 같이 라이브러리들이 주입되는데 이 중 유의깊게 봐야할 것들은 총 3개가 있다.
org.hibernate:hibernate-core
org.geolatte:geolatte-geom, org.locationtech.jts:jts-core
그 밖에 log 관련 라이브러리와 PostgreSQL 라이브러리가 들어오는데, PostgreSql 라이브러리가 들어오는 이유는 유추하기로 WKB/WKT를 위한 Default Dialects가 Postgis에서 비롯되어서인듯 하다.(?)
위의 사진을 보면, jts 의존성 주입 부분을 주석 처리 해놓은 것을 볼 수 있는데, 이는 hibernate spatial에 대해서 하나도 모르고 구글링으로 찾은 관련된 라이브러리를 다 받아서 사용해보던 중 org.locationtech.jts 라이브러리를 별도로 추가해야만 사용할 수 있다고 착각한 흔적이다. (hibernate 5.2 이전 버전까지는 별도로 추가했어야 되는 것 같긴 한데 정확히는 잘 모르겠음. 만약 자신이 사용하는 hibernate 버전이 5.6.5가 아니라면 나처럼 Dependency Tree를 확인하여 함께 설치되었는지 꼭 확인해보자.)
표준
이다.geometry
to a JTS `geometry, for instance, doesn’t require copying of the coordinates. It also delegates spatial processing to JTS. - 공식 문서Geometry
이다. 이로 인하여 처음 구글링을 생각없이 하다 보면 지금 내가 사용하고 있는 Geometry가 둘 중 어느 Geometry
인지 알 수 없게 된다. 아니 정확하겐 둘이 다른지 조차 모르고 사용한다. (둘을 변환할 순 있어도 서로 다른 자료형이므로 에러가 발생함.)Geometry
가 무엇인지는 정확히 알고 있어야 공부를 할 때 당황하지 않을 수 있다. (나 역시 같은 Geometry
인데 왜 안 될까 하면서 시간을 많이 허비하였다.)MariaDB와 MySQL 두 경우 모두 서버 자체는 실행이 되나 발생하는 에러가 좀 다르다.
MariaDB의 경우
MySQL의 경우
콘솔에 정직하게 Spatial 관련 에러가 발생함.
WKT란 공간 데이터를 표현해주는 텍스트 마크업 언어이다.
JTS에선 WKTReader().read(text)를 활용하여 이를 읽어들일 수 있다.
@Test
@DisplayName("WKT 읽기")
void hibernate_spatial_test() throws ParseException {
String pointFormat = String.format("POINT(%f %f)", 129.175759994618, 35.1710366410643);
String lineStringFormat = String.format("LINESTRING(%f %f, %f %f)", 129.20790463400292, 35.182416023937336, 129.16123271344156, 35.14426110121965);
String polygonFormat = String.format("POLYGON((%f %f, %f %f, %f %f))", 129.20790463400292, 35.182416023937336, 129.16123271344156, 35.14426110121965, 129.20790463400292, 35.182416023937336);
Geometry point = wktToGeometry(pointFormat);
Geometry lineString = wktToGeometry(lineStringFormat);
// polygon : startPoint와 endPoint가 일치해야만 함
Geometry polygon = wktToGeometry(polygonFormat);
assertThat(point.getGeometryType()).isEqualTo("Point");
assertThat(lineString.getGeometryType()).isEqualTo("LineString");
assertThat(polygon.getGeometryType()).isEqualTo("Polygon");
}
/*
WKT를 읽어들이는 메소드
*/
private Geometry wktToGeometry(String text) throws ParseException {
return new WKTReader().read(text);
}
Geometry Type을 원하는 모양으로 만들 수 있게 해주는 클래스.
모든 도형이 가능한 것은 아니고 가능한 메소드가 미리 구현되어 있다.
각각의 메소드 명이 매우 직관적이라 이해하기가 쉬운 편이다.
Geolatte에서 이 클래스에 해당하는 기능을 아직 찾지 못하였다.
/*
GeomertricShapeFactory를 활용하여 원을 만듬
*/
private Geometry createCircle(double x, double y, double radius) {
GeometricShapeFactory factory = new GeometricShapeFactory();
factory.setNumPoints(32); // 만들어진 Geometry 내부에 생성되는 Point의 최대 개수
factory.setCentre(new Coordinate(x, y));
factory.setSize(radius * 2);
return factory.createCircle();
}
@Test
@DisplayName("WKT 읽기")
void fromWkt_test() {
String pointFormat = String.format("POINT(%f %f)", 129.175759994618, 35.1710366410643);
String lineStringFormat = String.format("LINESTRING(%f %f, %f %f)", 129.20790463400292, 35.182416023937336, 129.16123271344156, 35.14426110121965);
String polygonFormat = String.format("POLYGON((%f %f, %f %f, %f %f))", 129.20790463400292, 35.182416023937336, 129.16123271344156, 35.14426110121965, 129.20790463400292, 35.182416023937336);
Geometry<?> point = Wkt.fromWkt(pointFormat);
Geometry<?> lineString = Wkt.fromWkt(lineStringFormat);
// polygon : startPoint와 endPoint가 일치해야만 함
Geometry<?> polygon = Wkt.fromWkt(polygonFormat);
assertThat(point.getGeometryType()).isEqualTo(GeometryType.POINT);
assertThat(lineString.getGeometryType()).isEqualTo(GeometryType.LINESTRING);
assertThat(polygon.getGeometryType()).isEqualTo(GeometryType.POLYGON);
}
JTS와는 달리 Geolatte의 Geometry는 제네릭으로 선언되어있으며, 제네릭의 타입으로 설정할 수 있는 특별한 추상 클래스가 Position이다.
Position을 상속받은 클래스들 (쉽게 말해 Position의 종류)
각각의 클래스에 들어가보면 매우 친절하게 설명이 다 적혀있으나 M, V 그리고 M이 붙어있는 클래스들의 용도는 아직 직접 사용해본 적이 없어 감이 잘 오지 않는다.
Position을 활용하여 엔티티에 Point를 생성하면 아래와 같다.
@Column(columnDefinition = "Point")
private Point<G2D> point;
Geloatte에서 Geometry 객체를 생성할 수 있는 도메인 특화 언어.
DSL.java 클래스를 들여다보면 상세히 설명이 적혀있으므로 자세한 설명은 생략하고 예시를 바로 보자.
@Test
@DisplayName("DSL 사용")
void dsl_test() {
Point<G2D> point = point(WGS84, g(4.33,53.21));
LineString<G2D> lineString = linestring(WGS84,g(4.43,53.21),g(4.44,53.20),g(4.45,53.19));
Polygon<G2D> polygon = polygon(WGS84,ring(g(4.43,53.21),g(4.44,53.22),g(4.43,53.21)));
assertThat(point.getGeometryType()).isEqualTo(GeometryType.POINT);
assertThat(lineString.getGeometryType()).isEqualTo(GeometryType.LINESTRING);
assertThat(polygon.getGeometryType()).isEqualTo(GeometryType.POLYGON);
}
위처럼 WKT를 사용하지 않고 Geometry 객체를 메소드로 직접 생성할 수 있다.
이 클래스의 from 메소드를 활용하면 JTS Geometry 객체를 Geolatte Geometry 객체로 아주 간단하게 변경할 수 있다.
/*
GeomertricShapeFactory를 활용하여 원을 만듬
*/
private Geometry createCircle(double x, double y, double radius) {
GeometricShapeFactory factory = new GeometricShapeFactory();
/*
만들어진 Geometry 내부에 생성되는 Point의 최대 개수
용도를 아직 잘 모르겠음
*/
factory.setNumPoints(32);
factory.setCentre(new Coordinate(x, y));
factory.setSize(radius * 2);
return factory.createCircle();
}
// 이 Geometry는 JTS의 Geometry이다!!
Geometry circle = createCircle(lat, lon, dist);
org.geolatte.geom.Geometry<?> geoLatteCircle = JTS.from(circle);