위의 화면은 사용자가 distance를 들어왔을 때 가장 먼저 볼 수 있는 화면이다.
저 화면에 보이는 유저 4명은 총 3가지(비로그인 유저, 로그인한 유저, 로그인은 했지만 GPS추적을 거부한 사람)유형으로 구분하여 DB에서 불러오는 방식을 사용했다.
오늘 개선할 부분은 바로 로그인한 유저의 매칭 부분을 개선할 것입니다.
public MatchResponseDto getNotFoundPositionMatchList(Member centerUser) {
List<MatchUserDto> userDtoList = memberReader.findMemberList().stream()
...
.collect(Collectors.toList());
return getMatchResponseDto(userDtoList);
}
현재는 위의 코드처럼 DB를 FullScan해서 조건에 맞게 걸러낸 다음 메인페이지 화면처럼 사용자에게 보여주고 있었다.
하지만 위의 방식은 사용자가 많아지는 경우에는 조회성능이 안 좋아지는 단점이 있다.
실제로 매칭 API를 호출했을 때 1.3s나 걸리는 것을 확인할 수 있었다.
그래서 알아보던 중 공간인덱스(SPATIAL INDEX)에 대해서 알게 되었고, 딱 지금 상황에 어울린다고 생각하여서 도입하기로 하였다.
Mysql에서 제공하는 공간타입은 크게 단입타입으론 Point, LineString, Polygon가 있고, 그들을 혼합해서 사용하는 MultiPoint, MultiLineString, MultiPolygon이 있다.
저는 좌표공간의 거리만 필요했기 때문에 Point를 활용했습니다.
공간데이터는 Mysql에서 제공하는 공간함수를 통해 활용할 수 있다.
전 이 중 ST_Contains, ST_Buffer, ST_GeomFromText 공간함수를 활용해 공간데이터를 사용했습니다.
함수 | 설명 | 주의사항 | 반환 값 |
ST_Contains | 첫 번째 Geometry가 두 번째 Geometry를 완전히 포함할 때만 True | - 경계에 걸쳐있으면 False 반환 - 포함 여부만 판단 |
1 (True) 또는 0 (False) |
ST_Buffer | Geometry 주변에 버퍼 영역(Polygon) 생성 | - 미터 단위 변환 : 거리 / 111319.9 - SRID에 따라 거리 단위가 다름 (4326은 경도/위도 단위) |
POLYGON |
ST_GeomFromText | WKT 형식 문자열을 Geometry 객체로 변환 | - POINT(경도 위도) 순서 유지 - latitude는 -90 ~ 90, longitude는180 ~ 180 - 순서가 틀리면 오류 발생 |
POINT, LINESTRING, POLYGON 등 |
여기서 WKT(Well-Known-Text)는 함수 명처럼 공간데이터를 텍스트로 변환해주는 함수를 의미한다.
SRID
공간데이터를 활용할 때 반드시 필요한 SRID는 데이터의 좌표계를 구분할 수 있는 고유한 숫자 코드다.
SRID | 단위 | 특징 |
4326 [ WGS 84 (GPS 표준) ] | 도 (degree) | - 위도(latitude), 경도(longitude) - Google Maps 기본 좌표계 |
3857 [ Web Mercator (Google Maps 표준) ] | 미터 (meter) | - Google, Bing, OSM 지도에서 사용 - 평면 투영 (유럽 및 북미에서 정확) |
5186 [ 한국 TM 중부 (Naver, Kakao 지도) ] | 미터 (meter) | - 한국 중심 - 국토지리정보원에서 사용 |
하지만 Mysql은 테이블을 생성할 때 위 설정을 해주지 않으면 SRID가 적용되지 않고 우리가 하고 싶은 공간인덱스 또한 사용할 수 없다.
저 또한 csv파일을 통해 더미데이터를 member Table에 import하려고 했지만 SRID가 설정되어 있지 않아 아무리 공간인덱스를 사용하려고 해도 할 수 인덱스를 사용하지 않았다..
location POINT NOT NULL SRID 4326,
위처럼 mysql은 location이라는 Point타입의 컬럼을 생성할 때 직접 SRID를 지정해줘야한다.
위처럼 SRID가 정상적으로 지정된 것을 확인할 수 있다.
이제 공간인덱스를 생성해보겠다.
ALTER TABLE member
ADD SPATIAL INDEX idx_location (location);
이 한줄만 입력하면 Index가 생성되는 것을 확인할 수 있다.
SHOW INDEX FROM MEMBER; 를 통해 인덱스가 정상적으로 생성되었는지 확인해볼 수 있다.
공간 인덱스(SPATIAL Index)란?
공간 인덱스(Spatial Index)는 2차원 공간 데이터를 효율적으로 검색할 수 있도록 설계된 인덱스로서, 일반 인덱스와는 다르게 좌표(Point), 선(LineString), 다각형(Polygon) 등 공간 데이터 타입에 특화되어 있다는 특징이 있다.
MySQL에서는 R-Tree (Bounding Box) 구조를 사용하여 공간 검색을 빠르게 수행한다.
Mysql에서 인덱스는 B-tree를 사용한다면 공간 인덱스에선 R-tree를 사용한다.
R-Tree
- R-Tree (Rectangle Tree)는 공간 데이터를 계층적인 사각형 영역(Bounding Box)으로 분할하여 저장하는 트리 자료구조.
- 범위 검색이나 교차 여부 같은 공간 쿼리를 효율적으로 처리.
R-tree는 크게 3가지의 계층 구조로 구성되어있다.
- Leaf Node
- 실제 데이터 객체(Point, LineString, Polygon)를 저장.
- 각 데이터 객체는 하나의 Bounding Box로 감싸져 저장.
- Internal Node
- 하위 노드들을 포함하는 Bounding Box로 저장.
- Leaf 노드 또는 다른 Internal 노드를 참조.
- Root Node
- 전체 공간 데이터를 포함하는 최상위 Bounding Box.
R-Tree 동작 방식
- 만약 특정 좌표(현재 내 위치)에서 반경 1Km 내에 있는 사용자들을 조회 하는 경우인 경우
- Root Node에서 시작하여 검색 영역과 겹치는 Internal Node로 이동.
- Internal Box 내부의 Leaf Box 중에서 검색 영역과 겹치는 Box만 선택.
- Leaf Box에 포함된 데이터 객체가 검색 영역에 포함되는지 확인.
- 포함된 데이터만 반환.
실제로 Distance에 공간인덱스를 적용해보자.
EXPLAIN
SELECT *,
ST_Distance_Sphere(
location,
ST_GeomFromText('POINT(37.4021735 127.1002747)', 4326), 1000
) AS distance
FROM member
LIMIT 4;
이렇게 하면 인덱스가 당연히 잘 적용될 지 알았다...
하지만 결과값을 보면
type = ALL로 FullScan을 한 것을 알 수 있다.
알아보니 ST_Distance_Sphere는 두 지점 간의 구면거리를 계산하는 공간함수이다.
즉, 계산된 거리 값을 비교하거나 정렬해야하므로 모든 행에 대해 계산을 해야하기 때문에 인덱스 적용이 안되었던 것이다..
왜냐하면 위에서 설명했듯이 공간인덱스는 R-Tree구조로서 포함여부를 판단하는 방식이다.
그래서 전 공간함수 중 ST_Contains과 ST_Buffer 공간 함수를 사용해 아래 쿼리문처럼 수정했습니다.
EXPLAIN
SELECT *
FROM member
WHERE ST_Contains(
ST_Buffer(
ST_GeomFromText('POINT(37.4021735 127.1002747)', 4326),
1000
),
location
)
ORDER BY RAND()
LIMIT 4;
그런 후 결과를 보니 정상적으로 인덱스가 적용된 것을 확인할 수 있었다.
위의 쿼리는 Point를 중심으로 1Km 이내에 있는 사용자들을 조회하는 것을 의미한다.
여기서 이상한 점을 찾아보자.
바로 ST_Buffer부분에 range부분이다.
분명 아까 ST_Buffer는 "미터 단위 변환 : 거리 / 111319.9"를 해야한다고 명시했었느데 제가 짠 쿼리에선 그런게없다.
왜 그럴까?
공식문서에 보면 8.0이상부턴 SRID 4326 (WGS 84)에서는 미터 단위로 거리를 계산하며, 다른 SRID에서는 다른 단위를 사용할 수 있다라고 나와있다.
공간 인덱스를 적용한 후 API호출을 해보니
1.3s -> 98ms로 약 92%감소한 것을 확인할 수 있었다.
이번 기회에 공간인덱스를 처음 사용해보면서 부족한 지식이지만 새롭게 알아가는 재미를 다시 한번 느낀 기회가 된 거 같다.
물론 아직은 더 깊게 공부를 해야겠지만 새삼 오늘도 스스로의 부족함을 많이 느끼고 더 성장해야겠다 라는 생각이 들었다.
'Refactor' 카테고리의 다른 글
[Distance] 짝퉁 카톡(KakaoTalk)을 만들어보자. - SSE (0) | 2024.10.29 |
---|---|
[Distance] SQS를 도입해보자. (0) | 2024.10.24 |
[TDD] Filter를 통해 사용자의 Repo를 감시하자. (0) | 2024.09.08 |
[Refactor] - TDD 서비스 코드를 간결하게 해보자,,! (0) | 2024.07.24 |