Hibernate Spatial을 이용한 좌표간 거리계산 (feat. PostGIS, H2GIS)

2022.11.20
7분
댓글

hibernate logo


💽 Hibernate Spatial


Hibernate Spatial은 지리 데이터를 계산하기 위해 만들어 졌고, Hibernate 5.0 버전 부터 Hibernate 라이브러리에 공식적으로 마이그레이션이 됐다.

현재 지원하는 데이터베이스는 Oracle, PostgreSQL, MySQL, MSSQL, H2이고, 각 데이터베이스에 구현 되어있는 지리 데이터처리 구현체를 추상화한 인터페이스가 Hibernate Spatial이다.

Hibernate Spatial은 JTS
geolatte-geom
이라는 기하학 모델을 제공한다고 한다.

이러한 GIS(Geometry Information System)를 Native Query로 날리지 않고 Hibernate에 추상화된 함수를 통해 JPQL로 쉽게 짤 수 있다.


👈 어떤 데이터베이스를 사용해야 할까?


hibernate function

Hibernate Spatial Function


Geometry 함수를 지원하는 데이터베이스는 많이 있다. 하지만 이 포스팅에서 작성할 내용인 ST_Distance, ST_Dwithin과 같은 좌표 간 거리를 간단하게 반환해주는 함수를 사용하기엔 이식성이 떨어지는 데이터베이스가 존재한다.

원래는 MySQL을 사용하려 했지만, 위 거리계산 함수를 Hibernate에서 지원하지 않아 Native Query로 사용해야 돼 ORM을 사용하는 이유를 찾지 못해 PostreSQL을 사용하기로 했다.

또한 테스트를 위해 H2 Database를 사용해야 하는데, H2GIS가 PostGIS를 기반으로 제작되어서 테스트가 용이한 이유도 있다.


⬇️ PostgreSQL, PostGIS 설치


우선 원하는 PostgreSQL을 설치하고, PostgreSQL로 지리계산을 하려면 PostGIS 라이브러를 설치해야 한다.

PostgreSQL 설치 방법과 버전은 너무 간단하니 패스!

PostGIS Relase


postgis relase

위 링크에서 릴리즈 버전을 선택할 때 지원하는 PostgreSQL 버전을 꼭 확인해야 한다!


PostgreSQL, PostGIS 설치가 모두 끝났으면, Extension 설정을 다음과 같이 해줘야 한다.

CREATE EXTENSION postgis;

이제 데이터베이스에 지리데이터 계산 함수를 사용할 수 있고, 지리데이터 타입을 사용할 수 있게 된다.

PostGIS 지리 함수에 대해선 아래 링크를 참고해보자

PostGIS Special Functions Index


✍️ Hibernate 설치 및 설정


우선 Gradle설정을 해준다.

plugins {
    id 'org.springframework.boot' version '2.7.5'
}

dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-web'
  implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
  implementation group: 'org.hibernate', name: 'hibernate-spatial'
  implementation group: 'org.postgresql', name: 'postgresql'
  runtimeOnly 'org.postgresql:postgresql'
}

Spring은 2.7.5버전을 사용할 예정이고 Hibernate는 spring-boot-starter에 명시된 버전을 사용할 것이다.

그리고 hibernate-spatial, jpa, postgresql implementation을 추가해준다.

Spring boot 버전 별 라이브러리 의존성은 스프링부트 공식 페이지에 잘 나와있다!

Spring boot 2.7.5 라이브러리 의존성은 여기서 확인


⚙️ application.yml 설정


spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/test_db # DB host
    username: username # DB username
    password: password # DB password
    driver-class-name: org.postgresql.Driver # DB driver
  jpa:
    hibernate:
      ddl-auto: create # 작성된 entity에 따라 스키마 자동 생성
      use-new-id-generator-mappings: true # pk 설정
    properties:
      hibernate:
        format_sql: true
        use_sql_comments: true
    # 중요!!
    database-platform: org.hibernate.spatial.dialect.postgis.PostgisDialect

위에서 중요한 부분은 spring.jpa.database-platform이고, 이 부분은 Hibernate Spatial의 지리 함수를 썼을 때 PostGIS 함수로 매핑해주는 Dialect 설정이다.

PostgreSQL 10버전 이상부터는 위 코드의 Dialect를 사용하면 된다고 한다.

PostgreSQL 버전에 따라 Dialect가 다른데 이는 아래 링크에 첨부하겠다.

Package org.hibernate.spatial.dialect.postgis


🔌 JPQL로 작성한 GIS 함수



...

import org.locationtech.jts.geom.Point;
import javax.persistence.*;

@Entity
public class Test {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", unique = true, nullable = false)
    private Long id;

    @Column(name = "name", nullable = false)
    private String name;

    @Column(name = "gps", nullable = false)
    private Point gps;

}

우선 거리 계산을 위해 Test 엔티티를 작성해 주고, Geometry 타입인 Point 타입을 이용해 컬럼을 작성해 준다.


public interface TestRepository extends JpaRepository<Test, Long> {

    /*
        dwithin 함수는 반경(단위: m) 내에 존재하는지 boolean 값으로 응답함
        4번째 인자는 평면에서 구할 것인지, 구에서 구할 것인지 정하는 flag로 false인 경우 구에서 거리를 계산함
    */
    @Query(value = "select t from Test t where dwithin(t.gps, :point, 30000, false) = true")
    List<Test> findTestByDistance(@Param("point") Point point);

}

그 뒤 Repository에서 JPQL을 사용하여 where절에 GIS 함수인 dwithin을 사용해 데이터베이스에 저장된 좌표와의 거리계산을 통해 List로 응답을 받게 코드를 작성한다.

Dwithin이란? (PostGIS ST_Dwithin)


Hibernate에 정의된 내용에 따르면, 두 Geometry(Point)타입과 Double 형인 distance를 사용하여 두 좌표간 거리가 distance 내에 포함되는지 boolean 형태로 리턴해 주는 함수이다.

distance 기준은 미터이다.


st_dwithin

PostGIS description


위 레포지토리 코드를 보면, 4번째 인자로 boolean이 추가되었는데

이는 주석에 적혀있는 것처럼 에서 거리 계산을 할 것인지, 평면에서 계산할 것인지 나타내는 값으로 위도, 경도를 이용한 거리 계산이 필요하다면 true로 인자를 전달하면 된다.