읽기에 앞서

  1. 총 4개의 포스팅으로 이루어져 있습니다.
  2. 이 게시글은 Spatial DB 자체에 대하여 심도있게 공부하고 정리하는 목적이 아닌 구글링을 통해서 얻을 수 있는 정보가 많이 없고 오래된 경우가 허다하여 직접 Spatial Data를 사용하기 위하여 공부하고 알게 된 것들을 정리하는 목적임을 미리 알려드립니다.
  3. Hibernate Spatial 공식 문서는 5.6 버전으로 설정되어 있습니다. (최신 버전은 Spatial 설명이 좀 부실한 것 같습니다 ㅠㅠ) 보다 최신의 문서를 보고 싶으시면 좌측 상단의 current로 버전을 변경해서 확인하세요.
  4. Dependency Tree 보는 법

dependency tress

개발 환경

  • JPA Hibernate 5.6.5
  • QueryDSL 5.0.0
  • MariaDB 10.6.10
  • 빌드 관리 도구 : Gradle


Spatial DB란?

  • 위키피디아 링크
  • 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를 반환


JPA Hibernate와 Spatial Data

Hibernate Spatial

  • Hibernate Spatial 공식 문서

  • Hibernate에서 Spatial Data를 사용하려면 org.hibernate:hibernate-core 의존성 외에 아래처럼 별도의 의존성, org.hiberate:hibernate-spatial을 추가해줘야만 한다.

  • 공식 문서에 따르면 Hibernate 5.0에 들어서야 정식 Hibernate ORM Project로 소속되었다고 한다.

    hibernate spatial dependency

  • 아래 트리와 같이 라이브러리들이 주입되는데 이 중 유의깊게 봐야할 것들은 총 3개가 있다.

    hibernate spatial dependency tree

  • org.hibernate:hibernate-core

    • hibernate 핵심 기능들이 들어있음
  • org.geolatte:geolatte-geom, org.locationtech.jts:jts-core

    • hibernate spatial에서 지원하는 Geometry Model
  • 그 밖에 log 관련 라이브러리와 PostgreSQL 라이브러리가 들어오는데, PostgreSql 라이브러리가 들어오는 이유는 유추하기로 WKB/WKT를 위한 Default Dialects가 Postgis에서 비롯되어서인듯 하다.(?)

    • For historical and practical reasons. The default dialects for WKB/WKT are those used in Postgis.
    • Geolatte Github 참고
  • 위의 사진을 보면, jts 의존성 주입 부분을 주석 처리 해놓은 것을 볼 수 있는데, 이는 hibernate spatial에 대해서 하나도 모르고 구글링으로 찾은 관련된 라이브러리를 다 받아서 사용해보던 중 org.locationtech.jts 라이브러리를 별도로 추가해야만 사용할 수 있다고 착각한 흔적이다. (hibernate 5.2 이전 버전까지는 별도로 추가했어야 되는 것 같긴 한데 정확히는 잘 모르겠음. 만약 자신이 사용하는 hibernate 버전이 5.6.5가 아니라면 나처럼 Dependency Tree를 확인하여 함께 설치되었는지 꼭 확인해보자.)

공간 데이터 타입 지원 라이브러리: JTS vs Geolatte

  • 앞서 살펴봤다 싶이 hibernate spatial 라이브러리를 추가하면 2개의 공간 데이터 타입을 지원해주는 라이브러리가 별도로 들어온다. 이에 대해서 알아보자.

그전에 잠깐!

  • 구글링을 하다보면 작성된지 좀 지난 문서의 경우 jts의 패키지 경로가 org.locationtech.jts.geom이 아닌 com.vividsolutions.jts.geom인 경우가 있다.
  • 아래 링크에서 확인할 수 있듯이 vividsolutions이 이전 버전의 산물이고 2016년 11월 3일 기준으로 the Eclipse Location Tech working group으로 이관(?)되어 개발되고 있다고 한다. 이로 인해 이관일 이후에 배포된 라이브러리 패키지명 또한 함께 바뀐 것이다.
  • 2022년 11월인 작성일 기준에서 vividsolutions 시절 기능을 사용할 일은 없다고 판단하므로 hibernate spatial 라이브러리만 추가했을 때 별도의 Geometry Type이 함께 추가되지 않는다면 org.locationtech.jts.geom을 받아서 설치하면 될 것 같다.
  • 참고 링크 - Difference in JTS from vividsolutions and locationtech

JTS

  • Spatial data types are not part of the Java standard library, and they are absent from the JDBC specification. Over the years JTS has emerged the de facto standard to fill this gap. - 공식 문서
  • 공식 문서에서 보면 알 수 있듯이, JTS는 사실상 Spatial data type의 표준이다.
  • 공식 링크

Geolatte Geom

  • Geolatte-geom (also written by the lead developer of Hibernate Spatial) is a more recent library that supports many features specified in SQL/MM but not available in JTS (such as support for 4D geometries, and support for extended WKT/WKB formats). Geolatte-geom also implements encoders/decoders for the database native types. Geolatte-geom has good interoperability with JTS. Converting a Geolatte geometry to a JTS `geometry, for instance, doesn’t require copying of the coordinates. It also delegates spatial processing to JTS. - 공식 문서
  • Geolatte Geom 또한 마찬가지로 Hibernate Spatial 쪽에서 개발한 것이며, JTS에 비하여 더 최신 라이브러리이기에 보다 더 많은 기능을 지원한다고 나와있다.
  • Geolatte Geom Github

그래서 뭘 써야할까?

  • 사실 이 부분에 있어서 정답은 없다고 생각한다. 두 라이브러리 모두 최근까지도 Maven Repository에 업데이트 됐을 정도(JTS - Jun 21, 2022, Geolatte Geom - Oct 08, 2022) 로 꾸준하게 업데이트되고 있을 만큼 기술적인 지원이 꾸준하기 때문이다.
  • 나의 경우, 두가지 모두를 사용해본 후 결국 Geolatte를 선택했는데 이유는 아래와 같다.
    • 보다 최신에 나온 라이브러리이므로 지원하는 기능이 더 많음.
    • JTS의 기능과 상호운용성이 매우 좋음.
  • 조심!
    • 두 Geometry 타입 모두 기본 자료형이 Geometry이다. 이로 인하여 처음 구글링을 생각없이 하다 보면 지금 내가 사용하고 있는 Geometry가 둘 중 어느 Geometry인지 알 수 없게 된다. 아니 정확하겐 둘이 다른지 조차 모르고 사용한다. (둘을 변환할 순 있어도 서로 다른 자료형이므로 에러가 발생함.)
    • 두 타입을 모두 사용하는 것은 아무런 문제가 없으나 정확하게 본인이 사용하는 Geometry가 무엇인지는 정확히 알고 있어야 공부를 할 때 당황하지 않을 수 있다. (나 역시 같은 Geometry인데 왜 안 될까 하면서 시간을 많이 허비하였다.)

Dialect 설정

  • 의존성 주입을 완료한 후, 한가지 설정을 더 해줘야 한다. 바로 사용하는 DB 벤더에 해당하는 Dialect를 설정해주는 것인데 기본적으로 Default로 설정되는 Dialect에선 Spatial Data를 지원하지 않으므로 본인의 버전에 맞는 Dialect를 찾아서 본인 프로젝트 설정 파일에 아래와 같이 설정해주면 된다.

spatial dialect

  • 현재 MariaDB 버전이 10.6이지만 지금 사용하는 hibernate spatial에서 지원하는 MariaDB용 SpatialDialect의 최신 버전이 10.3이라 사용 중이며 현재까지 문제없이 정상 작동 중이다.
  • 대부분의 DB 벤더에 따른 Dialect 패키지는 공식 문서에 명시되어 있으나 MariaDB의 경우 별다른 설명이 없어서 당황하였다. (참고할 자료 자체를 찾는게 너무 어려웠음)
  • 만약 나처럼 MariaDB를 사용 중이거나 본인의 DB 벤더에 알맞은 Dialect를 찾고 싶다면 아래의 사진처럼 라이브러리를 직접 들어가서 확인해보길 바란다.

dialect 패키지

만약 Dialect를 선언해주지 않는다면?

  • MariaDB와 MySQL 두 경우 모두 서버 자체는 실행이 되나 발생하는 에러가 좀 다르다.

    • (다른 DB 벤더는 확인해보지 못했습니다. 만약 어떤 에러가 발생하는지 아시는 분들은 저한테 가르쳐주시면 감사하겠습니다!)
  • MariaDB의 경우

    • 아무런 에러도 발생하지 않음. 쿼리도 정상적으로 날라가는 것을 확인하였음. 그래서 정상 작동이 되는 줄 알고 놀라워 했으나, 데이터 자체가 하나도 조회되지 않음.
    • 추후에 작성하겠지만, MBRContains는 지원 Dialect에 없는데도 동작이 됨. 따라서 Dialect와 관계없는 기본적으로 제공하는 기능으로 알았으나 데이터가 조회되지 않는 것을 확인하여 MBRContains가 왜 지원되는지 이유를 알 수 없어짐.
  • MySQL의 경우

    • 콘솔에 정직하게 Spatial 관련 에러가 발생함.

    • no spatial error


코드로 알아보자

  • 공부하면서 알게 된 JTS, Geolatte Geom(이하 Geolatte)에 관한 세팅과 사용법을 알아볼 차례다.
  • 다양한 기능들이 존재하지만 현재까지 내가 직접 사용하면서 검증이 된 것들 위주로 정리하고자 한다.

공통

  • 엔티티에 Geometry Type을 추가하려면 다른 Type들과 똑같이 원하는 Geometry 타입을 필드에 선언해주면 된다.
  • 이때, JTS와 달리 Geolatte는 Geometry Type이 Generic으로 선언돼있어서 타입을 명시해줘야 하는데, 이 부분은 아래에서 다시 정리하도록 하겠다.

JTS

1. WellKnownText (이하 WKT) 읽기

  • WKT - 위키피디아

  • WKT란 공간 데이터를 표현해주는 텍스트 마크업 언어이다.

    • ex) 좌표 (10, 20)에 점이 하나 찍혀있음 - POINT(10 20)
  • 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);
        }
    

2. GeometricShapeFactory

  • 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();
    }
    

Geolatte Geom

1. WKT 읽기

  • @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);
    }
    

2. Position

  • JTS와는 달리 Geolatte의 Geometry는 제네릭으로 선언되어있으며, 제네릭의 타입으로 설정할 수 있는 특별한 추상 클래스가 Position이다.

  • Position을 상속받은 클래스들 (쉽게 말해 Position의 종류)

    position

  • 각각의 클래스에 들어가보면 매우 친절하게 설명이 다 적혀있으나 M, V 그리고 M이 붙어있는 클래스들의 용도는 아직 직접 사용해본 적이 없어 감이 잘 오지 않는다.

    • G2D : Geographic Coordinate 즉, 지리 좌표계 (위도와 경도로 이루어진 좌표계)
    • C2D : Cartesian Coordinate 즉, 데카르트 좌표계 (수학에서 보던 x, y로 이루어진 좌표계)
    • G3D : G2D + Altitude (고도)
    • C3D : C2D + Z value (3차원)
  • Position을 활용하여 엔티티에 Point를 생성하면 아래와 같다.

  • @Column(columnDefinition = "Point")
    private Point<G2D> point;
    

3. CoordinateReferenceSystem Class (CRS - 좌표 참조 시스템)

  • CRS 문서
  • Geolatte는 WKT를 활용하지 않고 Geometry를 생성할 수 있는 DSL(도메인 특화 언어) Class를 가지고 있다.
  • 이 때, 생성할 Geometry의 기준 CRS를 정할 수 있는데, 이를 정의해놓은 클래스가 바로 CoordinateReferenceSystem Class이다.
  • 난 WGS84 (세계 지구 좌표 시스템)을 사용하였다. (G2D와 연동)

4. DSL (Domain Specific Language)

  • 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 객체를 메소드로 직접 생성할 수 있다.

5. org.geolatte.geom.jts.JTS

  • 이 클래스의 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);
    


댓글남기기