스프링부트

Spring Data JPA 에서 공간 정보(Spatial Data) 사용하기

알쓸개잡 2023. 11. 1.

Spatial Data Type은 공간 데이터를 저장하고 처리하기 위해 데이터베이스에서 제공하는 데이터 유형이다. 

이 데이터 유형은 지리적인 위치 정보나 공간 상의 객체를 나타내는 데 사용된다.

일반적으로 데이터베이스 시스템에서는 Spatial Data Type을 지원하기 위한 확장 기능이 제공된다.

Spring Data JPA + mariadb 간단한 샘플 코드를 통해서 Spatial Data Type을 사용하는 방법에 대해서 기록한다.

 

사전조건

  • 로컬 환경에 mariadb 데이터 베이스가 실행되고 있다고 가정하겠다.
  • mariadb 접속 정보 설정이 되어 있다고 가정하겠다.

SpringBoot 3.1 이상 버전의 경우 springboot docker compose를 사용하여 mariadb 설치 및 설정 없이 간단하게 테스트 환경을 구성할 수 있다.

springboot docker compose 사용에 대해서는 다음 포스팅을 참고하기 바란다.

2023.10.22 - [스프링부트] - spring boot 3.1 docker compose support

 

 

테이블 생성

 

CREATE TABLE `geometry_test` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `geo_point` geometry DEFAULT NULL,
  `geo_linestring` geometry DEFAULT NULL,
  `geo_polygon` geometry DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

point, linestring, polygon Spatial Data를 저장하는 테이블을 생성한다.

geometry 타입 대신에 각각 point, linestring, polygon으로 구체화된 타입으로 지정할 수도 있다.

 

Dependency

Spring Data JPA와 공간 데이터 처리를 위해서는 다음과 같은 추가 디펜던시가 필요하다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <version>3.1.0</version>
</dependency>
<dependency>
	<groupId>org.hibernate.orm</groupId>
	<artifactId>hibernate-spatial</artifactId>
	<version>6.2.2.Final</version>
</dependency>

parent  디펜던시로 spring-boot-starter-parent 가 지정되어 있다면 <version>은 생략 가능하다.

 

Entity and DTO 정의

Entity 정의

package com.example.jpa.entity;

import jakarta.persistence.*;
import lombok.*;
import org.locationtech.jts.geom.Geometry;

@Entity
@Table(name = "geometry_test")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@ToString(callSuper = true)
public class GeoMetryTest{
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "id")
	private Long gid;

	@Column(name = "geo_point", nullable = false)
	private Geometry point;

	@Column(name = "geo_linestring", nullable = false)
	private Geometry lineString;

	@Column(name = "geo_polygon", nullable = false)
	private Geometry polygon;

	@Builder
	public GeoMetryTest(Long gid, Geometry point, Geometry lineString, Geometry polygon) {
		this.gid = gid;
		this.point = point;
		this.lineString = lineString;
		this.polygon = polygon;
	}
}

point, lineString, polygon 모두 Geometry 타입으로 선언되었지만, 각각 Point, LineString, Polygon 타입으로 선언 가능하다.

 

DTO 정의

package com.example.jpa.dto;

import com.example.jpa.entity.GeoMetryTest;
import lombok.*;
import org.geolatte.geom.Point;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.Polygon;

@Getter
@Setter
@ToString
public class GeoMetryTestDto {
	private Long gid;
	private Geometry point;
	private Geometry lineString;
	private Geometry polygon;

	@Builder
	public GeoMetryTestDto(Long gid, Geometry point, Geometry lineString, Geometry polygon) {
		this.gid = gid;
		this.point = point;
		this.lineString = lineString;
		this.polygon = polygon;
	}

	public GeoMetryTest entity() {
		return GeoMetryTest.builder()
			.gid(gid)
			.point(point)
			.lineString(lineString)
			.polygon(polygon)
			.build();
	}
}

point, lineString, polygon 모두 Geometry 타입으로 선언되었지만, 각각 Point, LineString, Polygon 타입으로 선언 가능하다.

 

Repository 생성

package com.example.jpa.repository;

import com.example.jpa.dto.GeoMetryTestDto;
import com.example.jpa.entity.GeoMetryTest;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface GeoMetryTestRepository extends JpaRepository<GeoMetryTest, Long>{
	Optional<GeoMetryTest> findByGid(Long gid);
}

 

테스트

geometry_test 테이블에 샘플 데이터를 생성한다.

INSERT INTO geometry_test (geo_point, geo_linestring, geo_polygon) values(ST_GEOMFROMTEXT('Point(1 1)'), ST_GEOMFROMTEXT('LineString(1 1, 2 2, 3 3)'), ST_GEOMFROMTEXT('POLYGON((1 1,1 5,4 9,6 9,9 3,7 2,1 1))'));

 

테스트 코드

package com.example.jpa.repository;

import com.example.jpa.dto.GeoMetryTestDto;
import com.example.jpa.entity.GeoMetryTest;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.locationtech.jts.geom.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.annotation.Commit;

import java.util.List;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class GeoMetryTestRepositoryTest {
	@Autowired
	GeoMetryTestRepository geoMetryTestRepository;

	@Test
	@DisplayName("geometry 조회 테스트")
	void select_geometry_test() {
		Optional<GeoMetryTest> byGid = geoMetryTestRepository.findByGid(1L);
		Assertions.assertThat(byGid).isPresent();
		GeoMetryTest geoMetryTest = byGid.get();
		Geometry lineString = geoMetryTest.getLineString();
		Geometry point = geoMetryTest.getPoint();
		Geometry polygon = geoMetryTest.getPolygon();

		Assertions.assertThat(point).isInstanceOf(Point.class);
		Assertions.assertThat(lineString).isInstanceOf(LineString.class);
		Assertions.assertThat(polygon).isInstanceOf(Polygon.class);
	}

	@Test
	@DisplayName("geometry 저장 테스트")
	@Commit
	void save_geometry_test() {
		GeometryFactory geometryFactory = new GeometryFactory();
		Point point = geometryFactory.createPoint(new Coordinate(2.0, 2.0));
		List<Coordinate> coordinates = List.of(
			new Coordinate(1.0, 1.0),
			new Coordinate(2.0, 2.0),
			new Coordinate(3.0, 3.0)
		);
		LineString lineString = geometryFactory.createLineString(coordinates.toArray(new Coordinate[0]));
		List<Coordinate> coordinatesPolygon = List.of(
			new Coordinate(1.0, 1.0),
			new Coordinate(1.0, 5.0),
			new Coordinate(4.0, 9.0),
			new Coordinate(6.0, 9.0),
			new Coordinate(9.0, 3.0),
			new Coordinate(7.0, 2.0),
			new Coordinate(1.0, 1.0)
		);
		Polygon polygon = geometryFactory.createPolygon(coordinatesPolygon.toArray(new Coordinate[0]));
		GeoMetryTestDto geoMetryTestDto = GeoMetryTestDto.builder()
			.point(point)
			.lineString(lineString)
			.polygon(polygon)
			.build();

		GeoMetryTest entity = geoMetryTestDto.entity();
		GeoMetryTest save = geoMetryTestRepository.save(entity);
		Assertions.assertThat(save).isNotNull();
	}
}

select_geometry_test 테스트 코드 실행

select_geometry_test 를 실행하여 디버깅한 결과는 다음과 같다.

GeoMetry 타입의 lineString, point, polygon 변수의 인스턴스는 각각 LineString, Point, Polygon으로 저장된 데이터 유형에 맞게 적절한 객체 타입으로 변환됨을 알 수 있다.

 

save_geometry_test 테스트 코드 실행

Point, LineString, Polygon 인스턴스를 생성하여 저장해보는 테스트다.

실제 DB에 INSERT 되었는지 확인하기 위해서 @Commit 어노테이션을 지정하였다.

테스트 코드를 실행 후 geometry_test 테이블은 다음과 같다.


참고링크

https://mariadb.com/kb/en/geographic-geometric-features/

 

Geographic & Geometric Features

 

mariadb.com

 

댓글

💲 추천 글