개발일지

주변 사용자 조회 로직 변경

khw7385 2025. 3. 11. 21:14

앞의 포스트는 사용자의 위치 변경에 대한 내용이라면 이번 포스트는 주변 사용자 조회 로직 변경에 대한 내용이다.

 

주변 사용자 조회 요청에 대한 응답 데이터는 주변 사용자의 위치 정보와 최근 계획의 할 일 카테고리, 반경 내 모든 사용자의 카테고리 랭킹 정보로 구성된다. 각 데이터의 생성은 반경 내 주변 사용자가 많은 경우 서버에 문제가 발생할 수 있어 기존 로직을 수정하여 해결한다.

 

주변 사용자의 위치 정보 및 최근 계획의 할 일 카테고리 데이터 응답에 대한 로직 변경

기존 로직은 반경 내 모든 사용자의 위치 정보를 가져와 할 일 테이블에서 최근 종료된 계획에 대한 할 일을 모두 가져와 해당 데이터를 생성한다. 반경 내 모든 사용자를 가져온다면 사용자가 많을 시 클라이언트 단에서 사용자들을 모두 표시하기 어렵다는 문제가 존재한다. 서버 단에서는 모든 사용자에 대한 최근 계획의 모든 할 일을 조회해야 하므로 성능 상 문제가 일으킨다.

 

반경 내 주변 사용자가 많을 시 조사한 결과 두 가지정도의 처리 방식이 존재한다. 근접한 사용자를 클러스터링하여 처리하는 방식과 일부 사용자만 보여주는 방식이 있다.

 

기능의 특성 상 주변 사용자가 한 할 일이 중요하다고 생각하는데 클러스터링 방식으로 처리하면 클라이언트 단에서 카테고리 별로 필터링하기도 어렵고 해당 사용자의 할 일을 확인하기 위해서는 개별 사용자 데이터가 될 때까지 줌인(확대)해야 한다. 일부 사용자만 보여주는 방식은 보여줄 최대 인원수를 정하여 사용자의 수가 기준 값을 넘어간다면 기준 값의 수만큼만 보여주고 줌인(확대) 했을 때 반경을 작게하여 해당 반경에 해당하는 사용자를 다시 조회하는 방식이다. (줌인했을 때에도 해당 줄어진 반경에 대하서 사용자의 수가 기준 값보다 크다면 기준 값만큼만 보여준다) 이 방식은 줌인(확대)하지 않고도 개별 사용자의 할 일을 확인할 수 있기 때문에 기능에 더 어울리는 방식이다.

 

그러면, 주변 사용자의 수가 기준 값을 넘었을 때 보여줄 사용자를 선택하는 방법에 대해 생각해볼 필요가 있다.

주변 사용자를 선택할 기준이 되는 데이터를 고를 합리적인 정책이 있지 않아 사용자의 updated_at 데이터를 활용할 생각이다. updated_at 컬럼은 Member 테이벌의 데이터 변경 발생했을 때 기록하는 시간 값으로 사용자의 위치 정보를 수정했을 때 해당 값은 업데이트 된다. 사용자의 위치 정보가 현재 기능과 밀접하게 연관되어 있기 때문에 updated_at 을 사용해도 큰 무리가 없을 것 같다. 물론, 사용자의 프로필을 수정을 하면 updated_at 데이터가 변경되기는 하지만 프로필 수정과 위치 수정의 비율이 압도적으로 위치 수정이 많을 것 같아서 크게 문제가 될 것 같지는 않다.

 

응답으로 제공할 주변 사용자를 선택했으면 그 이후의 로직은 기존과 동일하다.

 

반경 내 모든 사용자의 카테고리 랭킹 정보 조회

기존의 방식과 크게 달라지는 부분은 이 로직이다. 기존 방식은 요청이 왔을 때 반경 내 모든 사용자들의 최근 종료된 계획의 할 일 데이터로부터 카테고링 랭킹 정보를 만들어낸다. 이 로직은 주변 사용자가 많은 경우 서버 성능에 큰 문제를 만들어낸다.

 

기존의 문제가 요청이 왔을 때 카테고리 랭킹 데이터를 생성하는 것이 문제였으니 미리 데이터가 만들어져 있다면 요청 시 해당 데이터를 조회만 하면 된다는 생각이 들어 집계 테이블로 생성하여 해결하는 쪽으로 방향을 잡았다. 하지만, 모든 사용자의 위치가 계속 변경되기 때문에 집계 테이블에서 한 번의 쿼리로 원하는 데이터를 만들어 낼 수 없다. 사용자에 대한 집계 데이터를 만드는 것은 불가능하다. 사용자가 아닌 위치에 대하여 집계 데이터를 생성하여 요청한 사용자를 기준으로 원하는 데이터를 만들어낸다.

 

1. 집계 테이블

집계 테이블의 스키마는 다음과 같다.

CREATE TABLE `cell_category_count` (
  `count` int NOT NULL,
  `category_id` bigint NOT NULL,
  `cell_id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  ...
  PRIMARY KEY (`category_id`,`cell_id`),
  CONSTRAINT `FK8s2nvmutjh8h1xaityc034591` FOREIGN KEY (`category_id`) REFERENCES `category` (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci

 

셀 id(geohash 7단계) 와 category_id 복합키를 기본키로 한다.

셀 id geohash를 7단계 값을 사용하는 이유는 6단계(1.22km * 0.61km) 는 셀의 크기가 커서 원의 반경의 형태와 크게 달라질 것이라고  생각을 하였고 8단계(38.2km * 19.1km) 은 사용자의 위치 변경으로 너무 많은 집계 테이블의 업데이트가 발생할 것 같아 중간이 7단계를 선택하였다.

 

2. Bounding Box 

사용자의 위치 데이터와 반경을 기준으로 Bounding Box를 구한다. Bounding Box는 반경 원을 내접하고 있는 사각형이라고 생각하면 된다. 경위도 좌표와 반경으로 Bounding Box 의 네 꼭짓점의 좌표를 구할 수 있다.

 

Bouding Box 의 네 꼭짓점 좌표를 구하는 방법은 다음과 같다.

 

위도 변화량 구하기 = ${\Delta \lambda_{\text{rad}} = \frac{d}{R \times \cos(\phi_{\text{rad}})}}$

경도 변화량 구하기 = ${\Delta \lambda = \arcsin\left(\frac{\sin\left(\frac{d}{R}\right)}{\cos(\phi)}\right)}$

 

BoundingBox 의 NE 좌표 (lat + 위도 변화량, lon + 경도 변화량)

BoundingBox 의 NW 좌표 (lat + 위도 변화량, lon - 경도 변화량)

BoundingBox 의 SE 좌표 (lat - 위도 변화량, lon + 경도 변화량)

BoundingBox 의 SW 좌표 (lat - 위도 변화량, lon - 경도 변화량)

 

3. BoundinBox 에 속하는 geohash(7단계) 구하기

BoundingBox의 네 꼭지점 좌표를 geohash(7단계)로 변환

 

네 geohash 를 기준으로 내부에 포함된 geohash 들 구하기

 

geohash 에서 부여되는 문자열은 규칙이 존재한다. 

홀수번째 해시(경도 정보 인코딩)의 경우는 왼쪽 이미지의 값처럼 부여되고 짝수번째 해시(위도 정보 인코딩)의 경우 오른쪽 이미지의 값처럼 부여된다.

 

따라서, geohash(7단계)에서 인접한 geohash값을 파악할 수 있고 BoundingBox 내부의 geohash 값들을 구할 수 있다. 하지만, BoundingBox는 원(반경)이 아닌 원을 포함하고 있는 사각형이다. 따라서, 실제 거리 정보를 이용해 원(반경) 안에 포함되지 않은 geohash는 제거한다.

 

public static List<GeoHash> deriveGeoHashInBoundingBox(GeoHash nwGeoHash, GeoHash neGeoHash, GeoHash swGeoHash, double latitude, double longitude, double radius) {
    GeoHash curRowHash = GeoHash.fromGeohashString(nwGeoHash.toString());
    GeoHash endRowHash = GeoHash.fromGeohashString(neGeoHash.toString());

    List<GeoHash> boundingBoxGeoHashes = new ArrayList<>();

    while(true){
        GeoHash curColHash = GeoHash.fromGeohashString(curRowHash.toString());
        GeoHash endColHash = GeoHash.fromGeohashString(endRowHash.toString());

        while(true){
            if (withinCircle(curColHash, latitude, longitude, radius)) {
                boundingBoxGeoHashes.add(curColHash);
            }

            if(curColHash.equals(endColHash)) break;
            curColHash = curColHash.getEasternNeighbour();
        }

        if(curRowHash.equals(swGeoHash)){
            break;
        }
        curRowHash = curRowHash.getSouthernNeighbour();
        endRowHash = endRowHash.getSouthernNeighbour();
    }

    return boundingBoxGeoHashes;
}

 

4. 카테고리별 랭킹 조회

3에서 구한 geohash 값을 통해 집계 테이블에서 geohash들에 해당하는 데이터를 조회한다. 조회된 데이터를 category id 별로 합해 카테고리별 랭킹 데이터를 생성한다.