[너나드리] 위치 정보를 어떻게 관리하고 사용할 것인가

eora21·2024년 3월 12일
0

너나드리 개발기

목록 보기
1/8

H3, MySQL Geometry

LBSNS(위치 기반 sns) 서비스를 만들기 시작했습니다. 사용자의 위치 정보를 얻어오고, 그에 맞는 게시글 등을 제공하려 합니다.
이 때, 제공하려는 서비스들의 위치 정보를 어떻게 관리할 것인지 고민해 봐야 했습니다.

H3

지구를 여러 개의 육각형(혹은 소수의 오각형) 구역으로 나누어 관리합니다. 해당 구역들은 각각의 고유한 인덱스를 지니고 있으며 long, String 타입으로 표기할 수 있습니다.
또한 하위 육각형 7개를 모아 상위 육각형 1개로 만들 수 있으며, 이를 resolution으로 나누어 표기합니다.

MySQL Geometry

MySQL의 Geometry로도 지도상의 위치를 나타낼 수 있습니다.
대표적으로는 Point가 있는데, 이를 이용하여 lat, lng을 하나의 데이터 타입으로 기입할 수 있습니다.
Polygon으로는 특정 영역을 지정하여 관리할 수도 있습니다.
또한 쿼리를 통해 특정 위치 주변 데이터들을 조회할 수도 있습니다.

무엇을 사용할 것인가

정확한 위치를 위해서는 당연히 MySQL의 Geometry를 써야 할 것입니다. 사용자가 특정 위치를 지정하여 글을 올릴 경우, Point를 이용하여 해당 위치를 정확히 표기할 예정입니다.

다만 여러 경우들을 생각해 봤을 때, h3도 같이 사용하기로 결정했습니다.

내 근처 조회

간단한 예시를 위해, 아무것도 없는 산 지형을 예시로 들어보겠습니다.
각각의 마커는 하나의 글이라 생각해주세요.

내 근처(주변 100m~200m정도)에 대한 정보를 얻을 경우, 이는 Geometry로도 충분히 조회할 수 있습니다.
주변에 데이터가 많을 경우 pagenation을 사용하여 적절한 개수의 마커를 표기할 수도 있습니다.
또한 조회 영역의 크기가 변화되더라도(100m -> 10m) 파라미터를 통해 적절히 조절할 수 있습니다.

내 중심 위치와는 거리가 있으나 특정 위치에 글이 많이 찍혔을 경우, 오히려 신뢰성을 높이는 척도(저 쪽이 굉장히 활발한 지역이구나)가 될 수도 있습니다.

즉, 내 근처만을 조회하는 경우에는 별다른 처리 없이 있는 그대로의 데이터들을 보여주면 될 것입니다.

다만 아래와 같은 경우는 다를 수 있습니다.

전체 지도 조회

전체 지도를 조회하는 경우(줌아웃하여 여러 장소에 대한 정보 조회를 요구하는 경우)에는 한 곳의 데이터만을 보여주면 오히려 굉장히 볼품없게 느껴질 수 있다고 생각했습니다.

정렬 기준을 무엇으로 하는가에 따라 다르겠지만, '가장 최신의 글 20개 조회'같은 경우 위와 같은 결과가 나와도 납득이 가능할 것입니다. 서울의 특정 장소에 사람들이 몰렸고, 이에 따라 해당 위치에서 20개의 글이 작성되었다면 이는 충분히 그럴 수 있는 상황입니다.

다만 일반적인 조회(특정 목적 없이 전국적으로 어떠한 글들이 있을까 하는 호기심)인 상황에서는 위와 같은 결과가 그렇게 좋지만은 않다고 생각했습니다.

사용자가 대전에서 데이터를 조회했으며, 대전에도 데이터가 다량 존재한다고 가정해봅시다. 지도를 전체적으로 조회했을 때, (막상 대전의 데이터가 조회되지 않았다 하더라도) 여러 위치에 마커가 찍힌 모습을 보고 싶어 할 것입니다.

조회되는 데이터의 수가 많고 적음과 관계 없이, 전국적으로 퍼져 있는 게 오히려 중요할 수 있습니다.

따라서 위와 같은 경우에는 데이터를 있는 그대로 보여주는 것이 아닌, 특정 기준으로 나눈 소량씩의 데이터를 보여주는 것이 더 낫다고 판단하였습니다.

여기서의 특정 기준을 h3로 선택하여 작업하기로 했습니다.

private final H3Core h3;

public Long toH3Index(Double lat, Double lng) {
    if (Objects.isNull(lat) || Objects.isNull(lng)) {
        return null;
    }
    
    return h3.latLngToCell(lat, lng, RESOLUTION);
}

h3 라이브러리를 사용하면 특정 위치에 대한 h3 cell 값을 쉽게 획득할 수 있으며, resolution 값으로 어느 정도의 거리 간격을 기준으로 할 지 선택할 수도 있습니다.

resolution = 5

붉은 색 육각형 하나가 하나의 cell이며, 해당 cell을 나타내는 Long값을 추출할 수 있습니다.

resolution = 6, 더 좁은 범위를 기준으로 인덱스 추출 가능

따라서 글을 저장할 경우, 해당 위치정보에 맞는 h3 cell을 구하고 db에 기입하면 될 것입니다.
db에 해당 컬럼에 대한 인덱스를 설정하고, 지도의 중심 위치를 기준으로 6~7개의 h3 cell과 일치하는 데이터들을 불러오면 여러 위치에 퍼져 있는 데이터를 조회할 수 있을 것입니다.

기준을 어떻게 잡을 것인가

구글 맵을 기준으로 특정 줌 레벨마다 조회 쿼리를 달리 하여 Geometry 혹은 h3를 통한 데이터를 얻어오기로 하였습니다.
따라서 모바일 환경의 화면비를 기준으로 잡고 작업해 보았습니다.

줌 레벨 12부터 시/군/구의 건물들이 한 눈에 보일 정도였으므로 12까지는 Geometry를 이용하기로 했습니다.

줌레벨 획득 영역
20 20m
19 40m
18 100m
17 200m
16 400m
15 800m
14 1km
13 2km
12 4km

(이는 필드 테스트 없이 진행한 것이며, 디폴트 줌 레벨(사용자의 화면에 기본적으로 제공될 줌 레벨)을 정하지 않았으므로 대략적인 정의일 뿐입니다. 추후 변동 가능성을 많이 열어 둔 상태입니다.)

이후에는 h3를 통해 범위를 파악했습니다.

줌레벨 11부터 구글맵의 ui가 전환되며, 특정 도로만 파악될 뿐 세세한 지역의 건물 등을 확인할 수 없습니다. 따라서 여기부터 h3를 사용하기로 했으며, resolution(이하 res)은 6입니다.

줌레벨 10은 res 6을 그대로 사용해도 무리가 없어 보입니다. 화면의 중심부에 각 데이터들을 확인할 수 있을 것입니다.
만약 모자르다고 느껴질 경우 테두리 한 칸씩을 더 늘려 총 19칸의 데이터들을 획득해도 좋겠으나 이는 추후 고려해보기로 했습니다.

줌레벨 9는 res 5가 적절해보입니다. 다만 너무 많은 인덱싱이 존재하면 db 성능 상 문제가 있을 수 있다 판단하여 res 4의 중간 육각형만 사용하거나 res 6에서 추가적인 연산을 통해 res 5를 구현할 생각입니다.

줌레벨 8~7 정도에서 대략적인 남한의 지형들이 다 보이기 시작했습니다. 따라서 8부터 res 4를 사용하도록 했습니다. res 4는 res 6처럼 db에 필드를 만들고 인덱싱을 걸었습니다.

줌레벨 7은 res 3이 과해 보입니다. res 4로 일단은 진행하다가, 이 역시 부족해 보인다면 추가적인 연산을 통해 res 3.5 정도로 맞출 생각입니다.

줌레벨 6에서는 res 3이면 충분해 보이며, res 3과 남한 가로 크기가 거의 일치하기에 이 이상까지의 작업은 하지 않을 예정입니다. 추후 해외 서비스를 가정한다면 추가적인 결정이 이루어져야겠지만, 오버 엔지니어링은 웬만하면 지향하는 방향으로 했습니다.

줌레벨 5부터는 대한민국이 아예 한 덩어리로 잡히기 때문에, 이후 설정은 줌레벨 6을 따라가기로 했습니다.

인덱싱 문제

조회 쿼리를 테스트해보던 중 문제가 생겼습니다. point 조회 쿼리에서 인덱싱이 걸리지 않았던 것입니다.

point에 srid를 gps 기본인 4326으로 걸어 주었습니다.

해당하는 인덱스가 잘 만들어진 것도 확인했습니다.

그러나, 막상 조회 쿼리를 확인하면 type ALL(풀스캔), key NULL(인덱스 적용 X)인 것을 확인할 수 있었습니다.
조회 쿼리도 ST_CONTAINSST_BUFFER를 사용했으므로, 이론적으로는 인덱스가 걸려야 했습니다.
possible_keys는 존재하는 걸 보아 쿼리가 인덱스를 제대로 사용하지 못 하는 것이리라 예상했습니다.
처음에는 데이터가 너무 적어서 그런가 싶어 10,000개를 추가했음에도 불구하고 동작하지 않았습니다.

혹시 몰라 st_within도 사용해봤으나, 내부 파라미터의 순서만 바뀐 것 뿐이므로 결과는 같았습니다.

추후 공식문서를 비롯한 여러 글들을 확인해 봤으나, 명확한 해결책은 보이지 않았습니다.
다만 8.0.31 버전 업데이트에서 8.0.29 버전부터 Geometry 인덱스가 적용되지 않는 문제를 파악했고, 이를 수정했다는 글이 있었습니다. Stackoverflow에서는 임시방편으로 강제 인덱스를 사용해보라는 조언이 있었기에 이를 적용해보았습니다.

MBRContainsST_CONTAINS와 같은 역할의 쿼리입니다.
위쪽 쿼리에서는 인덱스가 적용되지 않는 반면, 강제로 인덱싱을 태운 밑의 쿼리에서는 range가 보임을 알 수 있습니다.
또한 속도도 0.30초에서 0.00초로 감소한 것을 확인할 수 있습니다.

다만 강제로 인덱싱을 적용해야 한다는 점이 조금 걸리네요.. 더 좋은 해결책이나 근본적인 이유를 알게 된다면 추가적으로 포스팅해보도록 하겠습니다.

geographic SRS 문제

SRID 4326은 공간 데이터를 3차원 지구에 투영한 것을 뜻하며, 각각의 Point는 GPS 좌표를 뜻합니다. Google Map과 같은 웹 지도는 SRID 3857를 사용하지만, 저희 프로젝트 같은 경우 실제 GPS 좌표를 통해 판단해야 하므로 SRID 4326을 사용해야 합니다.

다만 SRID 4326과 같은 geographic SRS를 사용할 때는 몇가지 주의 사항이 필요합니다.

SRS 지원 여부

우선 MySQL 8.0 이전에는 SRID는 메타 데이터로만 간주되어 ST_ 함수들에 적용되지 않았습니다. 따라서 작성한 SRID와는 관계없이 무조건 2차원 좌표로만 측정되었습니다.

8.0으로 업데이트 된 후에도 몇몇 ST_ 함수들은 SRID와는 무관하게 동작했습니다.

ST_Buffer 같은 경우 저희도 사용 중이기에 릴리즈 노트를 살펴보니, 8.0.26부터는 지원된다고 합니다.

그 외 필요한 함수가 SRS를 지원하는지에 대한 여부는 릴리즈 노트를 검색해보시길 추천드립니다. 최신 버전 기준으로 정리된 글은 애석하게도 보이지 않네요..

좌표 순서

일반적인 좌표 순서는 lat-lng(위도-경도)입니다. 저 또한 그렇게 알고 있었습니다.

다만 GIS에서는 예외로 lng-lat(경도-위도)를 사용하며, MySQL 디스크에도 해당 순서로 저장된다고 합니다.

Stored as X=longitude, Y=latitude on disk
Default: SRS defined (predefined SRSs: latitude-longitude)
If you store angular coordinates, use X=longitude and Y=latitude

따라서 좌표 사용 시에는 lng-lat임을 잊어서는 안되겠습니다.

거리 계산

SRID 0, SRID 3857 같은 경우 POINT(0 0)POINT(1 1)의 거리 차이는 1.4142135623730951입니다. 이는 미터 단위로 계산됨을 알 수 있습니다.

그러나 SRID 4326 각좌표계를 사용할 경우 POINT(0 0)POINT(1 1)의 거리 차이는 156897.79947260793이 나옵니다. 이는 각좌표계 구조 상 '도' 단위로 저장되기 때문입니다.
물론 GPS 값을 그대로 저장하셔서 사용하신다면 문제는 없겠습니다.

지원 및 속도

MySQL이 geographic SRS에 대해 제대로 된 지원을 하지 못 한다고 들었으나, 릴리즈 노트 등을 살펴보았을 때 꾸준한 업데이트를 통해 많은 지원을 제공하고 있다고 생각합니다.
다만 속도 측면에서는 PostgreSQL의 PostGIS가 압도적으로 빠르고, (소문과 달리) MySQL보다 더 많은 문서가 존재하는 듯 보였습니다.

따라서 팀원들과의 회의를 통해 어떠한 DB를 사용할 지 결정해 보려 합니다.

Reference

https://h3geo.org/
https://developers.google.com/maps/documentation?hl=ko
https://tak2siva.github.io/
https://tecoble.techcourse.co.kr/post/2023-10-04-spatial-data/
https://dev.mysql.com/blog-archive/detecting-incompatible-use-of-spatial-functions-before-upgrading-to-mysql-8-0/
https://www.slideshare.net/NorvaldRyeng/mysql-80-gis-are-you-ready
https://dev.mysql.com/blog-archive/spatial-reference-systems-in-mysql-8-0/

profile
나누며 타오르는 프로그래머, 타프입니다.

0개의 댓글