ADR — 격자 계산을 위경도 근사에서 EPSG:5179 미터 좌표로 전환¶
요약
100m 격자의 기준을 위경도 등간격 근사(0.0009°×0.00115°)에서 EPSG:5179 평면좌표 floor(x/100)·floor(y/100) 로 바꿨다 (8/8 멘토링, MSG-347 구현 완료 · BE PR #134). 셀 동서 폭이 서울 101.6m·부산 104.7m로 흔들리던 것이 전국 100m로 고정된다.
파생 효과가 본체보다 크다: 새 셀은 위경도 축과 평행하지 않아(자오선 수렴 최대 약 1.6°) FE 렌더가 Rectangle→Polygon, 뷰포트 범위 산출이 SW·NE 2점→꼭짓점 4점으로 바뀐다. FE·BE는 proj4 정의 문자열 하나를 글자 단위로 공유한다.
grid_id 포맷("y_x" 문자열)·API 계약·표시명 규칙은 불변, 값만 교체(서울 41642_110458 → 19422_9582 대역). 저장 데이터는 V28 마이그레이션이 원본 좌표에서 전량 재계산.
이 노트로 답할 수 있는 질문¶
- 왜 위경도로 100m를 근사하면 안 되나? (지역마다 실제 거리가 달라 정사각형이 성립 안 함)
- 새 격자 계산 규칙과 FE·BE가 공유하는 계약은 무엇인가?
- grid_id를 단일 정수로 바꾸지 않은 이유는? (기각 근거 있음)
- 기존 데이터는 어떻게 옮겼나? 무엇까지 재계산되나?
- FE는 무엇을 바꿔야 하나? 검증은 어떻게 하나?
- zone(서면 A-14) 사각형은 다시 그렸나? (아니오 — 역산)
- 멘토가 제안한 H3 클러스터링은 왜 이번에 안 했나?
맥락¶
격자는 gridY = floor(lat/0.0009), gridX = floor(lng/0.00115)의 위경도 등간격 양자화였다. 위경도는 각도라 1도의 실제 거리가 위도에 따라 달라지고, 그래서 셀 동서 폭이 서울 약 101.6m · 부산 약 104.7m로 흔들렸다. 남북은 위도 무관이라 거의 일정. 즉 "100×100m 정사각형"은 한국 위도에 맞춘 근사였고, glossary도 이를 근사로 명시하고 있었다.
8/8 멘토링에서 이 방식을 지적받았다: 위경도에서 100m를 근사하지 말고 미터 기반 좌표계로 투영한 다음 100으로 나눈다. 함께 나온 주제는 FE/BE 계산 정합성 검증, Grid ID 설계, 표시명 분리, 줌 레벨별 집계·클러스터링, 검색 provider 통일이었다.
선택지¶
① 위경도 근사 유지 ② EPSG:5179 투영 후 100 분할 ③ H3 등 육각 격자 체계로 저장 단위 자체를 교체.
결정¶
② EPSG:5179. 좌표를 미터로 다루면 "100으로 나누면 100m"가 정의상 성립한다. ③은 멘토도 "저장용 기본 격자보다 조회·집계용 보조 인덱스가 적절"이라 했고, 사각 격자를 색칠하는 제품 UI와 모양이 어긋나 기각. ①은 문제 자체.
FE·BE 계약은 proj4 정의 문자열 하나다(글자 단위 동일해야 경계 셀이 안 어긋난다):
+proj=tmerc +lat_0=38 +lon_0=127.5 +k=0.9996 +x_0=1000000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs
BE는 Proj4J, FE는 proj4js를 쓴다. 정본은 GridConstants.CRS_DEF_EPSG5179 한 곳이고, FE 검증 픽스처(200건)에도 메타로 실려 나간다.
마이그레이션 SQL은 PostGIS ST_Transform에 SRID 5179를 넘기고, 이행 전에 두 경로(SRID·문자열)를 전국 221점에 실제로 돌려 같은 좌표가 나오는지 대조한다(최대 차이 1e-6m 초과 시 이행 중단, 현재 차이 0m). 원안은 "SRID 대신 문자열을 직접 전달"이었는데, 그러면 PostGIS가 호출마다 변환 경로를 새로 만들어 106배 느려진다(실측 0.846ms → 0.008ms). 2026-08-09 dev 배포에서 이행이 수 분간 멈춰 기동 헬스체크가 실패해 바꿨다. 문자열은 이제 변환 인자가 아니라 검사 기준으로 쓴다 — 어긋날 여지를 없앤다는 목적은 그대로다. spatial_ref_sys.proj4text 컬럼 비교로는 안 된다(auth_name='EPSG'면 PostGIS가 그 컬럼 대신 PROJ 내장 EPSG DB를 쓴다 — 실증 근거는 트러블슈팅 격자 이행 좌표변환 dev 배포 지연 MSG-347).
함께 내린 결정과 기각 대안¶
레포에는 결론만 남으므로 기각 근거를 여기 남긴다.
| 쟁점 | 결정 | 기각한 대안과 이유 |
|---|---|---|
| grid_id 형식 | "{gridY}_{gridX}" 문자열 유지 |
단일 정수 ID 기각 — 멘토가 인덱스 비용을 이유로 제시했으나, 성능이 걸리는 뷰포트 조회는 이미 정수 컬럼 grid_y/grid_x 범위 스캔이다(MSG-90 k6 확정). 이득은 MVP 규모에서 측정 불가인 반면 전 계층 타입 변경·FE 계약 파괴는 확실. 재검토 트리거: grids가 수천만 행이 되어 인덱스가 메모리를 압박한다는 실측. 재검토 시 인코딩은 Morton(Z-order) 1순위 — 2026-08-10 멘토가 Morton encoding(geohash의 인코딩 원리, bigint 하나)으로 재제안. 판정은 동일하게 "시점의 문제"다: 뷰포트는 이미 (grid_y, grid_x) btree BETWEEN 2개가 최적이고(Morton은 bbox가 Z-구간 여러 개로 분해돼 오히려 복잡), zone 매칭은 DB 없는 메모리 부등식 48건이라 이길 대상이 없으며(임의 사각형은 geohash prefix 매칭이 성립 안 함 — 2^k 정렬 타일 전용), 줌아웃 클러스터링도 (y>>k, x>>k) GROUP BY로 등가. 실이득은 PK+FK 3곳의 varchar 11~12B → bigint 8B 축소뿐이라 트리거 규모라야 체감된다. 단 트리거 발동 시엔 순번 정수가 아니라 Morton이 맞다 — 공간 지역성으로 인덱스 페이지 접근이 실제로 준다 |
| 검색 provider | 카카오 로컬 유지(무료) | 네이버로 통일 기각 — 멘토 권고는 지하철 출구·건물 출입구처럼 정밀 위치 정합이 중요할 때. 검색 결과는 100m 격자 매핑 입력이라 수 미터 오차가 무의미 |
| zone 사각형 재산출 | 확정 시드 48건을 위경도로 역산 후 재양자화 | 원천 파이프라인 재실행 기각 — 소진공 원천에서 다시 뽑으면 합집합·경계 조정·수동작도 31건 검수를 반복해야 한다. 작도 산출물에 WGS84 bbox가 소수 5자리로 보존돼(233건 전수 확인) 위치는 그대로 두고 번호만 다시 매길 수 있다 |
| 재양자화 방식 | 각 변을 가장 가까운 셀 경계로 스냅(상한 배타) | 꼭짓점 min/max floor 기각 — 변마다 1칸씩 부풀어 맞닿은 이웃 zone 7쌍이 겹치고 검수 스크립트가 FAIL(실측) |
| 좌표 범위 검증 | 기존 구현 확인 + 경계값 테스트만 | 신설 기각 — MSG-193의 KoreaCoordinates(위도 33~39·경도 124~132 닫힌 구간)와 에러코드 3400이 이미 두 쓰기 경로를 막고 있었다. 스펙 문구대로 격자 상수에 범위를 새로 두면 MSG-93이 없앤 중복 정본이 부활 |
| 구/신 격자 판별 | 임시 테이블 스냅숏 차집합 | 값 범위 휴리스틱(grid_y >= 30000) 기각 — 좌표를 지리적으로 제한하지 않는 계약과 충돌해 정상 격자를 구 격자로 오판할 수 있다 |
| 줌 집계·클러스터링(H3) | 후속 과제로 분리 | 멘토 제안이나 "화면당 100개 기준으로 먼저 성능 측정" 권고가 함께 있었고, 현재 뷰포트 API는 점령 격자만 커서 상한으로 내려준다. 실측으로 무거움이 확인되거나 축소 뷰 기획이 확정될 때 PRD부터 시작 |
영향¶
- FE (티켓 미발행 — 작업 시점에 BE 레포를 입력으로): proj4js 도입 → 계산식 교체 → 셀 렌더
naver.maps.Rectangle폐기, 꼭짓점 4점 역변환Polygon→ 화면 격자 범위도 4점 min/max. 검증은 BE 픽스처src/test/resources/fixtures/grid-epsg5179-samples.json(전국 200건: 입력 좌표·gridX/gridY·gridId·꼭짓점 4점) 전수 대조. 좌표 반올림 금지는 종전과 동일. - 무효화되는 기존 계약 서술: FE 격자 계약 프론트-백 합의와 ADR MSG-167 후속 결정 탐험률 축·격자 표시명·격자 계약의 "전역 고정 눈금 0.0009°×0.00115°" 산술은 이력이 된다. 계약의 나머지 골격(격자는 서버 리소스가 아닌 전역 눈금, DB엔 영상 올라온 칸만, API 5종, nullable 2종)은 그대로 유효.
- 불변: grid_id 포맷·DB 스키마 형태·API 경로/파라미터/응답 구조·표시명 명명 규칙("{zone명} {행}-{열}", 서버 계산 — MSG-341)·zones 캐시 용도. 값만 바뀐다.
- 데이터 이행(V28): 영상에 보존된 원본 좌표에서 전량 재계산 — 격자(중심점·경계 도형·행정동 라벨 재판정) · 영상 · 도감(최초 점령 시각 = 셀 소속 영상 최소 업로드 시각) · 스폰서 · 미션(병합 시 목표 수 보정) · 탐험률 통계 재계산. 셀 재편으로 사용자별 점령 격자 수는 줄 수 있고 뱃지는 회수하지 않는다(정식 배포 전).
- 배포 시 수동 조치: Redis
hotzone:*전체 삭제(랭킹 캐시 키에 TTL 없음) · FE 전환과 배포 시점 조율. - 미션 시드 원천 재산출은 이연 — 미션 기능 재개 시점까지 시딩 플래그 off 유지.
재검토 트리거¶
grids수천만 행 실측 → grid_id 정수화 재검토(원본 좌표가 보존되는 한 그때 전환 비용은 지금과 같다).- 정밀 POI 위치 정합이 제품 요구가 되면 → 검색 provider 통일 재검토.
- 뷰포트 응답 크기·렌더 부하 실측이 임계를 넘으면 → 줌 레벨별 집계와 H3 보조 인덱스.
- 서비스 범위가 한국을 벗어나면 → EPSG:5179 자체가 부적합(투영 원점이 한국 기준).