NestJS를 사용하는 프로젝트를 진행하다가 사용자의 경로 데이터를 저장해야 하는 상황이 생겨 공간 데이터를 사용하게 되었다. 공간 데이터 활용을 위해 공부했던 내용을 정리하고자 한다 💻
공간 데이터베이스란 말 그대로 공간 정보를 저장할 수 있는 데이터베이스를 뜻한다.
좀 더 명확한 개념을 이야기해보자면 공간에 존재하는 점, 선, 폴리곤등을 포함하는 객체의 데이터를 저장하고, 검색하는데 최적화된 데이터베이스라고 정의할 수 있다.
공간 데이터베이스는 단순히 공간 데이터를 저장해줄 뿐 아니라 공간 데이터를 활용한 공간 함수을 함께 제공한다.
Mysql에서는 공간 데이터를 저장할 수 있도록 타입을 제공하고 있다.
타입 | 정의 | 예시 |
---|---|---|
Point | 좌표 공간의 한 지점 | POINT(10 10) |
LineString | 다수의 Point를 연결해주는 선분 | LINESTRING(10 10, 20 20, 30 30) |
Polygon | 다수의 선분들이 연결되어 닫혀있는 상태 | POLYGON((10 10, 10 20, 20 10, 10 10)) |
Multi-Point | 다수의 Point 집합 | MULTIPOINT(10 10, 20 20) |
Multi-LineString | 다수의 LineString 집합 | MULTILINESTRING((10 10, 20 20), (15 15, 25 25)) |
Multi-Polygon | 다수의 Polygon 집합 | MULTIPOLYGON(((10 10, 10 20, 20 10, 10 10)), ((40 40, 30 30, 40 40))) |
GeomCollection | 모든 공간 데이터들의 집합 | GEOMETRYCOLLECTION(POINT(10 10), LINESTRING(20 20, 30 30)) |
Mysql에서는 다양한 공간 함수를 제공하고 있다. 그 중에서 자주 사용되는 함수들은 아래의 표와 같다.
공간 연산 함수 | 기능 |
---|---|
ST_Intersection (g1 Geometry, g2 Geometry) : Geometry | g1과 g2의 교집합인 공간 객체를 반환한다 |
ST_Buffer (g1 Geometry, d Double ) : Geometry | g1에서 d 거리만큼 확장된 공간 객체를 반환한다 |
ST_StartPoint (l1 LineString) : Point | l1의 첫 번째 Point를 반환한다 |
ST_EndPoint (l1 LineString) : Point | l1의 마지막 Point를 반환한다 |
ST_PointN (l1 LineString) : Point | l1의 n번째 Point를 반환한다 |
공간 관계 함수 | 기능 |
---|---|
ST_Equals (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를 반환한다 |
ST_Distance (g1 Geometry, g2 Geometry): Double | g1과 g2간의 거리를 반환한다 |
다양한 함수들이 많으니 공식 문서를 참고해서 원하는 기능의 함수를 잘 찾아서 사용하면 좋을 것 같다.
Mysql 공간 함수
NestJS에서는 어떻게 공간 데이터를 사용하고 저장할 수 있는지 경로 데이터를 저장하는 예시를 통해 알아보자.
NestJS version 9.0.0
TypeORM version 8.0.2
Mysql version 5.7
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
import { Point, LineString } from 'wkx';
@Entity()
export class Route {
@PrimaryGeneratedColumn()
id: number;
@Column({ type: 'point' })
startPoint: Point;
@Column({
type: 'linestring',
})
route: LineString;
}
TypeORM
과wkx
라이브러리를 사용하여 정의한 Route 엔티티이다.
여기서 사용한 wkx
는 Well Known Text
의 약자로, 이해하기 쉬운 문자열로 데이터를 표현하는 방식인데 GIS(공간 정보 시스템)의 공간 데이터의 Row 단위를 나타낼 수 있게 해준다.wkx
를 사용하면 mysql에서 지원하는 공간 데이터 타입들을 표현할 수 있기에 startPoint와 route 컬럼의 타입을 정의해주었다.
(npm을 통해 설치해주어야 사용이 가능하다 -> npm-wkx )
import { IsArray, ArrayMinSize } from 'class-validator';
type eachPoint = {
latitude: number;
longitude: number;
};
export class CreateRouteDto {
@IsArray()
@ArrayMinSize(2)
readonly routeData: eachPoint[];
}
프론트에서 전달받는 위치 정보는 latitude
와 longitude
로 이루어진 배열 데이터이기 때문에 eachPoint
타입을 정의해서 사용하였다.
경로 데이터는 2개 이상의 Point가 필수적으로 필요하기 때문에 @ArrayMinSize(2)
으로 설정해주었다.
async create(createRouteDto: CreateRouteDto) {
const { routeData } = createRouteDto;
const startPoint = `${routeData[0].latitude} ${routeData[0].longitude}`;
const route = routeData.map((v) => {
return `${v.latitude} ${v.longitude}`;
});
const linestring = route.join(',');
try {
await this.routeRepository
.createQueryBuilder()
.insert()
.into(Route)
.values({
startPoint: () => `ST_GeomFromText('POINT(${startPoint})')`,
route: () => `ST_GeomFromText('LINESTRING(${linestring})')`,
})
.execute();
} catch (err) {
throw err;
}
}
데이터를 DB에 저장하기 전에 Point 타입과 LineString 타입 데이터를 저장할 때에는 위의 표에 있는 예시와 같이 입력해줘야 하기 때문에 그에 맞게 데이터 형식을 바꿔주는 작업이 필요하다.
데이터 저장은 TypeORM의 queryBuilder
를 사용하여 세부적인 쿼리문을 작성할 수 있었다.
ST_GeomFromText
함수를 사용해서 문자열 데이터를 공간 데이터 타입으로 변환하여 저장하였다.
{
"routeData": [
{
"latitude": 0,
"longitude": 0
},
{
"latitude": 1,
"longitude": 1
}
]
}
DB에서 select문을 통해 결과를 확인하고 싶을 때에는 st_astext
함수를 사용할 수 있다!!
참고
https://orkhan.gitbook.io/typeorm/docs/entities#spatial-columns
https://dev.mysql.com/doc/refman/5.7/en/spatial-types.html
https://youngwoon.tistory.com/3
https://sparkdia.tistory.com/24