코코코딩공부

공간데이터와 공간인덱스로 지도 개선하기

Ocean_ 2023. 11. 3. 16:32

들어가며

요즘카페 서비스에 지도기능을 추가하기로 하였다.

사용자 설문조사 결과 지도와 관련된 피드백이 많았기 때문에 카페를 지도에 나타내기로 하였다.

초기에는 다음과 같이 좌표를 기준으로 원을 그리고 이를 탐색하기로 하였다. 원 안에 그려진 카페들을 핀을 통해서 보여주는 방식이다.

 

우리 서비스는 DB로 MySql을 사용하고 있는데 지도를 사용하기 위해 공간 정보를 담을 수 있는 공간데이터, 공간데이터를 효율적으로 사용할 수 있는 공간 함수, 검색 성능을 올릴 수 있는 공간 인덱스가 존재한다.

이를 활용하여 요즘카페 서비스의 지도 기능을 성능 좋게 만들어 보겠다.

 

MySQL 공간 데이터 다루기

MySql에는 여러가지 공간 데이터가 존재한다.

  • GEOMETRY
  • POINT
    • 좌표 공간의 한 지점
  • LINESTRING
    • 다수의 Point를 연결해주는 선문
  • POLYGON
    • 다수의 선분들이 연결되어 닫혀있는 상태

값 컬렉션으로는

  • MULTIPOINT
    • 다수의 포인트 집합
  • MULTILINESTRING
    • 다수의 LineString 집합
  • MULTIPOLYGON
    • 다수의 PolyGon 집합
  • GEOMETRYCOLLECTION
    • 모든 공간 데이터들의 집합

 

현재 개발하려는 기능은 각 카페마다 x, y 좌표를 가지고 있어야 하기 때문에, POINT 데이터 타입을 사용하기로 하였다.

 

CREATE TABLE cafe_coordinates
(
	id		 BIGINT NOT NULL AUTO_INCREMENT,
	coordinate POINT  NOT NULL,
	cafe_id	BIGINT NOT NULL,
	PRIMARY KEY (id),
	FOREIGN KEY (cafe_id) REFERENCES cafe (id)
);

CREATE SPATIAL INDEX idx_coordinates ON cafe_coordinates (coordinate);

카페 좌표를 저장할 테이블을 만들고, coordinate column에 인덱스를 걸어주었다.

공간 데이터에 인덱스를 걸 때는 공간 인덱스를 사용하였다. 이는 2차원의 데이터를 인덱싱하고, 검색하는 목적의 인덱스이다.

공간 인덱스(R-Tree)

우리의 MySql이 BTree로 관리 되듯, 공간 인덱스는 RTree를 통해 관리된다.

R-Tree는 점, 선, 면 과 같은 다차원 정보를 효율적으로 저장하기 위한 트리 자료구조이다.

R-Tree의 핵심은 MBR이다.

MBR

MBR이란 (Minimum Bounding Rectangle) 최소 경계 사각형 이다. 모든 다각형을 포함할 수 있는 사각형이라고 할 수 있다.이런 MBR을 만들고 노드로 관리하는 트리이다.

이런 성수역 골목이 있다고 해보자

 

지도를 없애고 공간데이터의 MBR을 판단해보면

이런식으로 공간데이터의 MBR이 산출될 수 있다. 그림을 대충 그렸다 .. 아래 이미지와 다를 수 있다.

대충 이런 이미지에서 다음과 같은 트리 구조가 나올 수 있다.

 

J라는 공간 데이터를 조회한다면 루트에서 노드를 따라 찾아가면서 전체 탐색을 하는 것보다 효율적으로 탐색을 진행할 수 있다.

쿼리 실행 계획 확인하기

좌표 테이블도 만들고 공간인덱스도 만들어놨으니 인덱스가 잘 작동하는지 확인해 본다.

MySql에서 제공하는 공간함수를 이용해서 쿼리를 작성할 수 있다.

공간 함수 관련 MySql문서

SELECT c.id, c.name, co.coordinate
FROM cafe_coordinate AS co
INNER JOIN cafe AS c
ON co.cafe_id = c.id
WHERE ST_CONTAINS(ST_Buffer(ST_PointFromText('POINT(37.54464 127.05589)', 4326), 200), co.coordinate);

다음과 같은 방식으로 조회해보면

 

다음과 같이 조회될 수 있다.

쿼리

위의 결과처럼 coordinate에 공간데이터가 저장되는데, 이는 공간함수가 값을 생성하는 것이다.

 

ST_PointFromText('Point(37.54464 127.05589)', 4326) 를 먼저 설명하자면,

쿼리에서처럼 Point(위도 경도) 와 같은 형식이 WKT인데, 이는 지리적인 개체의 위치와 형태를 설명하는 것이다.

이후 파라메터는 SRID 값인데, 이는 좌표 시스템이다. 4326으로 설정한 이유는 이는 전 세계적으로 GPS 시스템에서 사용되는 좌표계이며 지구의 형태를 곡면으로 모델링하는 데 사용되는 값이기에 사용하게 되었다.

 

ST_CONTAINS(ST_Buffer(ST_PointFromText('POINT(37.54464 127.05589)', 4326), 200), co.coordinate);

ST_Buffer 함수를 사용하여 원의 반경을 만든 다음, ST_Contains 함수를 사용하여 해당 반경 내에 있는 카페 좌표를 찾는다. 이를 통해 지정된 지점을 중심으로 반경 내에 있는 모든 카페를 검색할 수 있다.

 

기존에는

ST_DISTANCE_SPHERE(ST_PointFromText('Point(37.54464 127.05589)', 4326), co.coordinate) <= 200;< />trong>

위의 함수는 두 지점 사이의 거리를 구하는 데 사용한다. 구의 표면을 이용하여 두 지점 사이의 최단 거리를 계산하고 이를 미터로 환산해준다. 그래서 위의 값에서 받은 값과 좌표값들을 모두 비교해서 그 거리가 200m 이하일 경우에 조회를 시켜주는 방식이다.

 

인덱스를 잘 타고있다.

현재 100여개의 데이터에서는 유의미한 속도 차이가 존재하지는 않는다. 하지만 data가 많아진다면

인덱스 있을때

인덱스 없을때

와 같이 성능이 개선된 것을 확실하게 알 수 있다.

인덱스를 안타는 경우

위에서 언급한 SRID라는 값이 존재한다. DB의 Row별로 이 값이 다를경우 문제가 발생할 수 있다.

Table

CREATE TABLE cafe_coordinates
(
	id		 BIGINT NOT NULL AUTO_INCREMENT,
	coordinate POINT  NOT NULL,
	cafe_id	BIGINT NOT NULL,
	PRIMARY KEY (id),
	FOREIGN KEY (cafe_id) REFERENCES cafe (id)
);

테이블을 생성할 때 공간 데이터 column에 SRID 값을 적용시킬 수 있다. Default는 0으로 삽입된다.

ALTER TABLE cafe_coordinates MODIFY COLUMN coordinate POINT NOT NULL SRID 4326;

다음과 같이 수정하였다.

ROW

데이터 삽입도 SRID 값을 이용해주어야한다.

INSERT INTO cafe_coordinates (coordinate, cafe_id) VALUES (ST_PointFromText('POINT(10 20)', 4326), 1);

이 데이터 값은 MySql에서 alter 할 수 없어 데이터를 새롭게 추가해줘야한다.

https://dev.mysql.com/doc/refman/8.0/en/spatial-index-optimization.html

이 문서를 확인하면 모든 SRID 값이 일치해야 인덱스를 탈 수 있다.

코드 적용

전체 코드를 보여주기 보단, 몇가지 주의해야 될 점들에 대해 작성해보려한다.

build.gradle

implementation group: 'org.hibernate', name: 'hibernate-spatial', version: '6.2.5.Final'

hibernate5 부터 공식적으로 공간데이터를 지원하기 때문에 gradle에 의존성을 추가하였다.

new Coordinate()

public static Point generateWithCoordinate(final double latitude, final double longitude) {
	final Point point = GEOMETRY_FACTORY.createPoint(new Coordinate(longitude, latitude));
	point.setSRID(SRID);
	return point;
}

Point를 생성할 때 사용하는 메소드이다. Coordinate를 생성하는데 이 값이

다음과 같이 x,y 로 되어있는데 이는 경도, 위도 순이니 주의 해야한다 ..

당연하게 경도는 y축 위도는 x 축이라 고 생각했었는데 다르다.

 

핵심

해당하는 좌표를 받는다 → 지도 범위에 따른 StringPolygon을 생성한다 → 이 값을 이용해 DB에서 조회한다 → 반환한다.

 

비즈니스 요구사항 변경

기존에는 다음과 같이 원 안에 포함되어있는 카페를 반환하였다. 하지만 사용자들이 뷰를 보았을 때 지도 내에 있는 카페 전부가 뜨는 것을 기대하지 저 동그란 영역에서만 나올 것이라고 기대하지는 않을 것 같아서 구현을 변경하게 되었다.

해결

클라이언트로부터 지도 각 모서리의 좌표를 받는다. 좌표를 통해 위도 경도를 만들고 이를 이용해 폴리곤을 만든다.

final Coordinate[] vertexes = new Coordinate[]{
		new Coordinate(maxLongitude, minLatitude),
		new Coordinate(maxLongitude, maxLatitude),
		new Coordinate(minLongitude, maxLatitude),
		new Coordinate(minLongitude, minLatitude),
		new Coordinate(maxLongitude, minLatitude)
};

다음과 같이 vertex를 만들 수 있다. 4개의 좌표지만 5개가있는 이유는 닫힌 형태의 다각형을 만들기 위함이다!

 

@Query("""
		SELECT co.cafe.id AS id, co.cafe.name AS name, co.cafe.address AS address, ST_X(co.coordinate) AS latitude, ST_Y(co.coordinate) AS longitude
				FROM CafeCoordinate co
		WHERE ST_CONTAINS(:area, co.coordinate)
		""")
List<CafePinDto> findCafePinsFromCoordinate(@Param("area") final Polygon area);

다음과 같은 방식으로 JPQL로 공간함수를 이용할 수 있다.

MYSQL 5.6이상의 버전을 사용한다면 많은 공간함수를 사용할 수 있을 것이다. 이를 통해 entity로 받아올 수 있었다.

나가며

MySql에서 지원해주는 공간 기능이 이렇게 다양한지 몰랐다. 이를 통해 지도를 사용하는 방법에 대해 알 수 있었다.

폴로햄 감사합니다.