프로젝트 회고

Re:USE 프로젝트 中 근처 상품 조회 기능 구현

iksadnorth 2023. 11. 19. 16:32

👣 구현 배경

Re:USE 프로젝트는 의류 중고 거래 웹 서비스로서 사용자와 사용자가 직접 거래를 하기 위해선
판매자와 구매자의 거리가 짧을수록 좋을 것이라 가정했습니다.

때문에 사용자에게 추천하는 상품 중 거리가 가까운 상품을
보여줘야 할 필요성이 느껴져서 해당 기능을 구현하고자 기획했습니다.

사용자에게 받는 주소는 문자열 형식의 주소를 받게 되어 있으며
예를 들자면, " 서울특별시 중구 세종대로 110"와 같은 도로명 주소로 
입력되도록 유도했습니다.

 

👣 구현 시도 1st - 행정 구역 기준 카테고리화

처음에는 도로명 주소로 입력받은 위치를 시/도/동/구 등등으로 Parsing하고 
각 카테고리가 동일하면 동일할수록 거리가 가까운 것으로 가정하자는 논의가 이뤄졌습니다.

예를 들어, 3개의 주소가 주워졌다고 가정해보겠습니다.

1. 서울특별시 중구 세종대로 110
2. 서울특별시 중구 필동3가 28-19
3. 경기도 성남시 분당구 불정로 6

위에 제시된 3개의 주소를 행정 구역 단위로 Parsing하면 다음과 같습니다.

1. 서울특별시 | 중구 | 세종대로 | 110
2. 서울특별시 | 중구 | 필동3가 | 28-19
3. 경기도 | 성남시 | 분당구 | 불정로 | 6

1번과 2번은 "서울특별시"라는 단어와 "중구"라는 단어가 동일하므로 거리가 가깝다고 여길 수 있으며
1번과 3번은 겹치는 단어가 존재하지 않으므로 거리가 멀다고 여길 수 있습니다.

물론 이렇게 하면 대략적인 멀고 가까움을 알 수는 있겠지만 너무나도 치명적인 허점을 가지고 있었습니다.

1. 단어가 다르다고 해서 가까울 수도 있는 거리를 먼 거리로 치부할 수 있다.
예를 들어 "서울특별시 중구"라는 위치 A와 "서울특별시 서구"라는 위치 B,
"서울특별시 동구"라는 위치 C가 있다고 가정해보겠습니다.
"위치 A와 위치 B 사이의 거리"는 "위치 B와 위치 C" 사이의 거리보다 더 가까울 것입니다.
하지만 위에서 제시한 방법으로 계산하면 3개의 위치의 거리는 어디가 더 가까운지 측정하기 어렵습니다.

2. 행정 구역 단위로 Parsing하기가 너무 어렵다.
단순히 공백으로 행정 구역을 분리하기엔 변칙적인 것이 너무 많고
해당 방법이 언제나 유효하다는 법칙이 없습니다.
뿐만 아니라 애초에 입력받을 때, 도로명과 지번 모두를 받을 수 있는 구조기에
일관된 행정 구역으로 나누기도 애매합니다.

따라서 해당 방법은 논의 단계에서만 끝나게 되었습니다.

 

👣 구현 시도 2nd - 위도, 경도와 같은 위치값 활용.

두 번째 방법은 아예 해당 위치에 대한 위도, 경도와 같은 좌표값으로 변환해 거리를 계산하는 것이었습니다.
도로명 주소를 확실하게 위도, 경도값으로 바꿔만 준다면 그 이후의 계산은 평면 좌표계에서의 거리 계산 공식을
이용하면 쉽게 해결 가능합니다. 

해당 방식의 맹점은 도로명 주소와 같은 문자열 주소를 좌표값으로 변환할 수 있냐는 것이었습니다.
이것을 위해 외부 API를 찾아본 결과 "카카오 로컬 API"를 찾을 수 있게 되었습니다.

 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com

🐾 API 후보 1 - 주소 검색하기 (/v2/local/search/address.{format})

 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com

첫 번째로 고려한 위치 변환을 위한 API는 "주소 검색하기" API 였습니다.
단순하게 여러 API 중 프로젝트에서 요구하는 기능을 제공한다는
설명글로 인해 해당 API를 이용하고자 했습니다.

요청을 위한 Query String은 다음과 같았습니다.
query의 값으로 검색하고자 하는 도로명 주소를 넣으면
응답으로 좌표값을 전달하는 구조였습니다.

요청 Query String
응답 Json

해당 API는 원하는 대로 주소값을 위도, 경도로 바꿔줬습니다.
하지만 해당 API는 주어진 주소값을 위도, 경도로 바꿔주지 않는 경우가 의외로 많았습니다.

예를 들어, "카카오 본사"라는 키워드로 검색을 다음과 같이 경도, 위도로 변환해주지 못했습니다.

물론 해당 API로도 충분히 구현 가능하지만 저는 최대한 수용력이 큰 API를 사용하기를 원했습니다.
때문에 다음과 같은 API로 구현하기로 결정했습니다.

🐾 API 후보 2 - 키워드로 장소 검색하기 (/v2/local/search/keyword.{format})

 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com

요청을 위한 Query String은 다음과 같았습니다.
query의 값으로 검색하고자 하는 도로명 주소를 넣으면
응답으로 좌표값을 전달하는 구조였습니다.

해당 API는 관련성이 높은 위치를 순서대로 정렬하기 때문에 더 수용성있는 
구현이 가능해지기에 사용하기로 결정했습니다.

 

👣 구현 구체화 - DB 조회를 위한 통계 쿼리 구성 방법

위 과정을 통해 사용자에게 주소 데이터를 받으면
각 주소에 대한 위도, 경도 좌표값으로 변환할 수는 있었습니다.

이것을 위해 2가지 방법을 고려했습니다.

🐾 방법 1 - MySQL의  st_distance_sphere 함수를 이용

st_distance_sphere는 MySQL 5.7버전부터 제공하는 지리 함수로서
두 지리적 좌표 사이의 구면 거리를 계산하는데 사용됩니다.
실제 지구의 곡률값을 고려해서 거리를 계산합니다.

때문에 해당 방법은 아주 먼 거리에서의 위도, 경도 사이의 거리를 측정할 때도 
정확한 값을 도출할 수 있으며 MySQL에서 제공하는 함수이기 때문에
신뢰도가 높은 편 입니다.

SELECT ST_DISTANCE_SPHERE(
    POINT(40.748817, -73.985428), 
    POINT(34.052235, -118.243683)
) AS distance;

위와 같은 예시를 보면, 단순히 2개의 위치의 위도, 경도만 알면 거리를 측정할 수 있기 때문에 
사용법도 그렇게 까지 어렵지는 않다고 생각했습니다.

하지만, 해당 방식에는 2가지의 단점이 존재했습니다.

1. MySQL에서만 제공하는 함수라서 특정 벤더에 종속적.
ST_DISTANCE_SPHERE라는 함수는 JPA에서 제공하는 함수가 아닌 MySQL에서 제공하는 함수이기 때문에
H2 DB까지 사용하고 있던 프로젝트에서는 사용하기 어렵다는 판단을 내렸습니다.

2. 계산 비용이 높다.
조사 결과 해당 방식은 지구의 곡률까지 고려하는 3차원 좌표 거리를 계산하는 것이기 때문에
계산 비용이 다소 높은 편입니다. 만약 Re:USE 서비스가 국가와 국가 사이의 사용자들을 타겟으로
정한 것이었다면 지구의 곡률까지 고려해야 겠지만 전혀 그런 상황이 아니었기에
큰 문제가 될 것이라고 판단했습니다.

위와 같은 이유로 다른 방법을 고려하기로 결정했습니다.

 

🐾 방법 2 - 2차원 평면 좌표의 거리 공식 이용

2차원 평면 좌표계의 거리 공식을 이용해서 각 상품의 위치를 계산하기만 해도
'행정 구역 단위로 나눈 거리 측정'보다 더 정확할 것이고
'st_distance_sphere 측정 방식'보다 덜 정확하지만 보다 계산이 빠른 방식이 될 것입니다.

때문에 해당 방식을 통해 각 상품 사이의 거리 오름차순으로 API를 설계하고자 결정했습니다.

가독성을 높이기 위해 Order By절의 거리를 구하는 함수는
QueryBuilder라는 유틸리티 클래스로 따로 분리해서 구현했습니다.

해당 기능이 제대로 작동하는지 확인하기 위해 테스트 코드를 작성했고 
이는 제 예상대로 거리순으로 정렬해줌을 확인까지 했습니다.

1번째 member_location이 제외된 이유는 사용자 상품을 제외한 상품을 나열하기 위해서 입니다.