들어가며
요즘카페 서비스에 지도기능을 추가하기로 하였다.
사용자 설문조사 결과 지도와 관련된 피드백이 많았기 때문에 카페를 지도에 나타내기로 하였다.
초기에는 다음과 같이 좌표를 기준으로 원을 그리고 이를 탐색하기로 하였다. 원 안에 그려진 카페들을 핀을 통해서 보여주는 방식이다.
우리 서비스는 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에서 제공하는 공간함수를 이용해서 쿼리를 작성할 수 있다.
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에서 지원해주는 공간 기능이 이렇게 다양한지 몰랐다. 이를 통해 지도를 사용하는 방법에 대해 알 수 있었다.
폴로햄 감사합니다.
'코코코딩공부' 카테고리의 다른 글
이미지 리사이징 (0) | 2023.11.07 |
---|---|
Batch Insert에 대한 고민(feat. 2배 개선) (1) | 2023.11.01 |
필터를 사용해 API 성능 로그 만들기 (2) | 2023.10.31 |
테스트를 더 빠르게 진행시켜보자 (0) | 2023.10.21 |
TestContainer 사용기 & 테스트 격리 (1) | 2023.10.16 |