seungjun.dev

위치 정보를 DB에 저장하는 방법

위도 경도와 같은 정보를 DB에 어떻게 저장할까?

접근 방식

지도 정보를 데이터베이스에 저장하는 가장 일반적인 방법은 해당 지점의 위도경도 좌표를 저장하는 것이다.

크게 두 가지 기본 접근 방식이 있다.

  1. 숫자 컬럼 사용: DECIMAL, DOUBLE 같은 숫자 데이터 타입의 컬럼 두 개(e.g. latitude, longitude)에 위도와 경도를 각각 나누어 ****저장
  2. 공간 데이터 타입 사용: DB 자체에서 지원하는 ‘공간 데이터 타입’(Spatial Data Type)을 사용 예를 들어, POINT 라는 단일 컬럼에 (경도, 위도) 좌표 쌍을 하나의 값으로 저장

어떤 방식을 선택하는지에 따라 사용해야 할 DB 기능, 쿼리, 그리고 성능이 크게 달라진다.

MySQL에서 사용

MySQL 5.7부터 공간 데이터 기능을 지원하며, 8.0에서 관련 함수들이 대폭 개선됐다.

단순히 DECIMAL 타입으로 컬럼 두 개를 만드는 것보다, POINT 를 사용하는 것이 훨씬 효율적이다.

POINT 타입은 (X, Y) 좌표를 하나의 값으로 저장한다.

단, GIS 표준과 MySQL은 POINT(경도, 위도) 순서를 따른다. 우리가 흔히 말하는 “위도, 경도”의 반대이므로 주의한다.

-- 'locations'라는 이름의 장소 테이블 생성
CREATE TABLE locations (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    
    -- (경도, 위도) 순서로 좌표를 저장할 컬럼
    -- SRID 4326은 GPS가 사용하는 표준(WGS84)을 의미합니다.
    coords POINT NOT NULL SRID 4326
);

예제

강남역 (경도 127.0276, 위도 37.4979)을 기준으로 반경 5km 이내에 있는 모든 장소를 검색한다.

-- 1. 검색 기준이 될 '내 위치'를 POINT 객체로 설정합니다.
--    SRID 4326을 명시해줍니다.
SET @my_location = ST_GeomFromText('POINT(127.0276 37.4979)', 4326);

-- 2. 검색할 반경(미터 단위)
SET @radius_meters = 5000;

-- 3. 실제 검색 쿼리
SELECT 
    id, 
    name, 
    ST_AsText(coords) AS location_coords,
    -- ST_Distance_Sphere는 두 지점 간의 구(지구) 표면 거리를 미터(m) 단위로 계산합니다.
    ST_Distance_Sphere(coords, @my_location) AS distance_in_meters
FROM 
    locations
WHERE 
    -- ST_Distance_Sphere 함수로 계산한 거리가 @radius_meters보다 작거나 같은 것만 필터링
    ST_Distance_Sphere(coords, @my_location) <= @radius_meters;

SG_GeomFromText로 텍스트를 POINT 객체로 변환한다.

ST_AsTextPOINT 객체를 텍스트로 다시 변환해 준다.

ST_Distance_Sphere는 두 POINT 간의 구면 거리를 미터로 반환한다.

위에서 작성한 쿼리는 기능적으로는 완벽하지만, locations 테이블에 데이터가 100만 건 있다면 정말 느릴 것이다.

성능 문제 원인 - Full Table Scan

인덱스가 없는 상태에서 위 쿼리를 실행하면, MySQL은 다음 과정을 거친다.

  1. locations 테이블의 **모든 행(100만 건)**을 하나씩 전부 읽어옴
  2. 각 행마다 ST_Distance_Sphere 함수를 실행 (지구 곡률을 계산하는 복잡한 삼각함수 연산 등 수행)
  3. 값이 비싼 거리 계산을 100만 번 수행함
  4. 계산 결과가 5000 이하인 행만 최종 반환

인덱스

  • 테이블의 검색(SELECT) 속도를 획기적으로 높이기 위한 자료구조
  • 책에 색인을 등록하는 것과 같다

성능 문제 해결 - 공간 인덱스 (Spatial Index)

일반적인 인덱스(B-Tree)가 값의 대소 관계를 빠르게 찾는 데 특화되어 있다면, 공간 인덱스(주로 R-Tree)는 2D/3D 공간에서 어떤 영역 안에 포함되는지를 빠르게 찾는데 특화되어 있다.

-- coords 컬럼에 SPATIAL INDEX를 생성합니다.
ALTER TABLE locations ADD SPATIAL INDEX(coords);

공간 인덱스가 적용된 테이블에서 2번의 쿼리를 다시 실행하면, MySQL 옵티마이저는 훨씬 똑똑하게 동작한다.

  1. 쿼리가 ST_Distance_Sphere(...) <= 5000 조건을 확인
  2. 옵티마이저는 5km 반경의 원을 완벽하게 감싸는 가장 작은 사각형(MBR, Minimum Bounding Rectangle) 영역을 먼저 계산
  3. SPATIAL INDEX (R-Tree)를 사용하여, 이 사각형 영역(MBR) 안에 포함되는 행들만 빠르게 찾아냄
    • R-Tree 인덱스는 특정 사각 영역 내의 데이터를 찾는 데 매우 빠르다.
    • 이 단계에서 테이블의 나머지 데이터는 아예 연산 대상에서 제외된다.
  4. 인덱스를 통해 걸러진 소수의 후보군에 대해서만 ST_Distance_Sphere 함수를 실행하여, 사각형의 모서리에 있지만 실제 원 밖 5km를 벗어나는 데이터들을 최종적으로 탈락시킴
  5. 최종 결과 반환

B-Tree vs. R-Tree

  • B-Tree (Balanced-Tree): 숫자, 문자열 처럼 1차원적인(1D) 데이터를 정렬하고 검색하는 데 사용
  • R-Tree (Rectangle-Tree): 지도 좌표처럼 2차원(2D) 또는 3차원(3D)의 공간 데이터를 검색하는 데 사용

B-Tree

B-Tree는 이름 그대로 ‘균형 잡힌’ 트리 자료 구조이다. 일반적인 DB 인덱스의 표준이다.

특정 값 찾기, 범위 검색, 정렬이 주요 용도이다.

모든 데이터를 정렬된 상태로 유지하며, 마치 거대한 파일 캐비닛처럼 작동하는 계층을 가지고 있다.

  • 루트 노드는 "A-G", "H-M", "N-S", "T-Z" 같은 큰 범위를 가리킨다.
  • ‘K’로 시작하는 데이터를 찾으면, “H-M” 서랍을 연다.
  • 그 안에는 "Ha-Hj", "Hk-Hz", "I-J", "Ka-Ko", "Kp-Kz", "L-M" 같은 더 세분화된 폴더가 있다.
  • “Ka-Ko” 폴더를 열면 원하는 데이터가 있다.

여기서 균형이 잡혀있다는 것은, 어떤 데이터를 찾든 루트에서부터 특정 데이터까지의 탐색 깊이가 거의 일정하게 유지된다는 의미다.

이 덕분에 데이터가 아무리 많아도 항상 예측 가능하고 빠른 검색 속도(O(log N))을 보장한다.

R-Tree

R-Tree는 ‘사각형’ 트리이다. R-Tree는 정렬 대신 ‘그룹화’를 사용한다.

근처에 있는 데이터를 묶어서 그것들을 모두 포함하는 가장 작은 사각형(MBR)으로 표시한다. 그 다음, 이 ‘사각형’들을 다시 근처에 있는 것끼리 묶어서 더 큰 사각형으로 만든다. 이 과정이 반복되어, 루트 노드는 전체를 감싸는 거대한 사각형이 된다.

검색할 때는 R-Tree는 위 계층을 타고 내려간다.

예를 들어서 내 위치 반경 5km(검색 영역) 쿼리가 들어온다고 가정한다.

  • 내 검색 영역이 ‘서울’ MBR과 겹치는가? → O
  • ‘서울’ MBR의 자식 노드인 ‘강남구’, ‘송파구’, ‘마포구’ MBR 중 내 영역과 겹치는 것은? → ‘강남구’, ‘송파구’
  • ‘마포구’ MBR은 내 영역과 겹치지 않으므로, ‘마포구’에 속한 수천 개의 모든 하위 데이터는 아예 검색 대상에서 제외한다.
  • 이런 식으로 관련 없는 영역을 통째로 가지치기함으로써 엄청나게 빠른 공간 검색이 가능해진다.

공간 인덱스는 자동으로 적용되나?

  • 공간 함수를 사용하면 공간 인덱스가 자동으로 적용되나?

    • 모든 공간 함수가 인덱스를 활용하는 것은 아니다. WHERE 절에서 인덱스 친화적인 함수를 사용해야 쿼리 옵티마이저가 인덱스 사용을 고려한다.
  • 공간 인덱스가 사용되는 조건은?

    1. 대상 컬럼에 SPATIAL INDEX가 생성되어 있어야 함
    2. WHERE 절에서 인덱스 친화적인 함수를 사용해야 함
      • O (인덱스 사용 가능): ST_Contains, ST_Intersects, ST_Within 등 두 공간의 관계를 판단하는 함수
      • X (인덱스 사용 불가): ST_Distance, ST_Distance_Sphere 등 거리를 계산하는 함수
  • ST_Distance_Sphere (거리 계산)는 왜 인덱스를 못 쓸까?

    • 인덱스는 특정 영역 내의 데이터를 빠르게 찾는 구조
    • WHERE ST_Distance_Sphere(location, point) <= 5000 같은 조건은, DB가 모든 레코드와의 거리를 일단 계산해봐야 5000 이하인지 알 수 있으므로 인덱스를 활용하지 못하고 Full Table Scan이 발생
  • 인덱스 친화적인 함수를 쓰면 100% 인덱스를 타나?

    • 자동은 아니지만 쿼리 옵티마이저가 판단해서 사용
    • 옵티마이저는 데이터의 양, 조회 범위(선택도) 등을 비용 기반으로 계산하여, 인덱스를 사용하는 것이 Full Table Scan보다 빠르다고 판단될 때 인덱스를 사용
    • 대부분의 합리적인 상황(데이터가 많고, 좁은 범위를 조회)에서는 인덱스를 사용
  • 인덱스 사용 여부 확인법은?

    • 실행할 쿼리 앞에 EXPLAIN을 붙여 실행
    • 결과 테이블의 key 컬럼에 내가 만든 공간 인덱스의 이름이 표시되는지 확인 (typeALL이 아니어야 함)