지리 데이터 처리와 분석은 다양한 분야에서 중요한 역할을 합니다. 특히 지도 기반 서비스나 위치 기반 애플리케이션에서는 정확한 위치 정보의 빠른 조회가 필수적입니다. 이를 위해서는 지리적 셀 ID를 계산하고, 이를 효율적으로 관리하는 시스템이 필요합니다. 오늘은 H3와 S2Cell을 사용하여 지리적 셀 ID를 계산하는 방법과, 이를 DynamoDB에 저장하는 방식에 대해 살펴보겠습니다.
1. 폴리곤 vs S2/H3: 왜 셀 기반 시스템을 써야 할까?
많은 개발자들이 처음에는 폴리곤으로 지역을 관리하려고 하는데요, 실제 서비스에서는 S2나 H3 같은 셀 기반 시스템을 더 많이 사용합니다. 이유가 뭘까요?
폴리곤 방식의 한계
- 복잡한 지형을 정확히 표현할 수 있지만, 연산 비용이 매우 높음
- 인덱싱이 어려워 대량의 위치 데이터 처리가 비효율적
- 반경 검색이나 근접 지역 찾기가 복잡하고 느림
지리적 데이터를 효율적으로 다루기 위해서는 위치를 일정한 셀(그리드) 단위로 표현하는 것이 유리합니다. 이때 필요한 것이 바로 S2 셀 ID나 H3 셀 ID입니다.
- S2Cell?
- 구글의 S2 Geometry 라이브러리를 기반으로 한 셀 시스템으로, 지구 표면을 여러 격자 셀로 분할하여 각 셀에 고유한 ID를 부여합니다. S2Geometry는 주로 구글 지도와 관련된 시스템에서 사용되며, 2D 평면 대신 구면에서의 지리적 처리가 특징입니다.
- H3?
- Uber에서 개발한 Hexagonal Grid 시스템으로, 지구를 여섯 각형(육각형)으로 나누는 방식입니다. H3는 특히 지리적 반경 계산과 유연한 해상도 조정이 가능하여 최근에는 많은 위치 기반 서비스에서 인기를 끌고 있습니다.
S2 셀과 H3 셀을 사용하는 이유
- 데이터 간격의 일관성: 위치 기반 데이터에서 다양한 반경으로 검색할 때, S2나 H3 셀 ID를 활용하면 위치를 동일한 크기의 셀로 나눠 인덱스화할 수 있습니다. 이렇게 인덱싱해두면 반경 내 검색과 폴리곤 커버링이 효율적으로 이뤄집니다.
- 경계 근처 데이터 관리: 반경을 변경할 때 경계 근처의 데이터를 빠르게 찾으려면 인접 셀을 효율적으로 관리해야 합니다. S2와 H3는 이러한 경계 문제를 해결하는 데 용이합니다.
- 데이터 최적화: 셀 ID를 미리 계산해 저장해두면 DynamoDB 같은 비관계형 DB에서 데이터를 빠르게 조회할 수 있으며, 검색 속도가 최적화됩니다.
S2/H3의 장점
- 위치 데이터를 균일한 크기의 셀로 나눠서 효율적인 인덱싱 가능
- 빠른 반경 검색과 이웃 셀 찾기
- 확장성이 뛰어나 대규모 위치 데이터 처리에 적합
- DynamoDB 같은 NoSQL DB와 궁합이 좋음
서비스에 필요한 정보
반경을 변경할 때마다 해당 지역의 셀 ID에 해당하는 데이터를 조회하고 건물이나 매장 정보를 불러오려면 다음과 같은 정보가 필요합니다.
- 셀 ID: 지정된 반경을 덮는 모든 셀 ID가 필요합니다.
- 기본 위치 정보: 각 건물이나 매장의 위도, 경도 정보.
- 셀 ID 외 추가 정보: 셀 ID를 기반으로 조회된 각 데이터에 대해 위치와 관련된 건물의 상세 정보 (예: 건물 이름, 매장 수, 건물 유형 등).
- 검색 반경 또는 영역 정보: 변경된 반경이나 영역을 기반으로 커버링된 셀 목록을 즉시 계산할 수 있어야 합니다.
2. Spark와 H3, S2를 이용한 셀 ID 계산 예시
아래는 Spark에서 S2 셀과 H3 셀을 이용해 폴리곤을 커버링하는 셀 ID를 구하는 예시입니다.
참고 사항
- UDF는 Python 함수를 호출하므로 대규모 데이터셋에서는 성능 저하가 있을 수 있습니다.
- 서비스별로 레벨(해상도)값은 실험이 필요함.
예시 코드: S2를 사용하여 셀 ID 계산
from s2sphere import CellId, LatLng
from pyspark.sql import SparkSession
from pyspark.sql.functions import udf
from pyspark.sql.types import StringType
# Spark 세션 시작
spark = SparkSession.builder.appName('S2_Cell_Calculation').getOrCreate()
# UDF 등록 (S2 셀 ID 계산 함수)
def calculate_s2(latitude, longitude, level):
lat_lng = LatLng.from_degrees(latitude, longitude)
cell_id = CellId.from_lat_lng(lat_lng).parent(level)
return str(cell_id)
# UDF를 Spark에 등록
s2_udf = udf(calculate_s2, StringType())
# 예시 데이터 생성 (위도, 경도, 해상도)
data = [(37.7749, -122.4194, 12), (34.0522, -118.2437, 12), (40.7128, -74.0060, 12)]
columns = ['Latitude', 'Longitude', 'Level']
df = spark.createDataFrame(data, columns)
# S2 셀 ID 계산
df_with_s2 = df.withColumn('S2_Cell_ID', s2_udf(df['Latitude'], df['Longitude'], df['Level']))
# 결과 출력
df_with_s2.show()
예시 코드: H3를 사용하여 셀 ID 계산
import h3
from pyspark.sql import SparkSession
from pyspark.sql.functions import udf
from pyspark.sql.types import StringType
# Spark 세션 시작
spark = SparkSession.builder.appName('H3_Cell_Calculation').getOrCreate()
# UDF 등록 (H3 셀 ID 계산 함수)
def calculate_h3(latitude, longitude, resolution):
return h3.geo_to_h3(latitude, longitude, resolution)
# UDF를 Spark에 등록
h3_udf = udf(calculate_h3, StringType())
# 예시 데이터 생성 (위도, 경도, 해상도)
data = [(37.7749, -122.4194, 9), (34.0522, -118.2437, 9), (40.7128, -74.0060, 9)]
columns = ['Latitude', 'Longitude', 'Resolution']
df = spark.createDataFrame(data, columns)
# H3 셀 ID 계산
df_with_h3 = df.withColumn('H3_Cell_ID', h3_udf(df['Latitude'], df['Longitude'], df['Resolution']))
# 결과 출력
df_with_h3.show()
+---------+----------+--------+--------------------+
| Latitude| Longitude| Level | H3_Cell_ID |
+---------+----------+--------+--------------------+
| 37.7749 | -122.4194| 9 | 8928308297fffff |
| 34.0522 | -118.2437| 9 | 8928308280fffff |
| 40.7128 | -74.0060 | 9 | 89283082cfffff |
+---------+----------+--------+--------------------+
Pyspark에서 dynamodb-geo를 사용한 S2 셀 ID 계산 예시
ffrom dynamodb_geo import GeoDataManager, GeoPoint
from s2sphere import CellId, LatLng
from pyspark.sql import SparkSession
from pyspark.sql.functions import udf
from pyspark.sql.types import StringType
# Spark 세션 시작
spark = SparkSession.builder.appName('DynamoDB_S2_Cell_Calculation').getOrCreate()
# DynamoDB와 연결된 GeoDataManager 초기화
geo_data_manager = GeoDataManager(table_name='GeoTable')
# UDF 등록 (S2 셀 ID 계산 함수)
def calculate_and_store_s2(latitude, longitude, level):
# S2 셀 ID 계산
lat_lng = LatLng.from_degrees(latitude, longitude)
cell_id = CellId.from_lat_lng(lat_lng).parent(level)
# DynamoDB에 저장할 GeoPoint 객체 생성
geo_point = GeoPoint(latitude, longitude)
geo_point.add_property("CellID", str(cell_id))
# DynamoDB에 GeoPoint 저장
geo_data_manager.put_point(geo_point)
return str(cell_id)
# UDF를 Spark에 등록
s2_dynamo_udf = udf(calculate_and_store_s2, StringType())
# 예시 데이터 생성 (위도, 경도, 해상도)
data = [(37.7749, -122.4194, 12), (34.0522, -118.2437, 12), (40.7128, -74.0060, 12)]
columns = ['Latitude', 'Longitude', 'Level']
df = spark.createDataFrame(data, columns)
# S2 셀 ID 계산 및 DynamoDB에 저장
df_with_s2 = df.withColumn('S2_Cell_ID', s2_dynamo_udf(df['Latitude'], df['Longitude'], df['Level']))
# 결과 출력
df_with_s2.show()
3. DynamoDB에 데이터 적재하기
예시 코드: DynamoDB에 S2 셀 ID 데이터 적재
DynamoDB에 셀 ID를 활용한 지리 데이터를 미리 계산해 저장합니다.
import boto3
from s2sphere import CellId, LatLng
# DynamoDB 테이블 생성
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('GeoTable')
# 데이터 적재 예시
latitude = 37.7749
longitude = -122.4194
cell_id = CellId.from_lat_lng(LatLng.from_degrees(latitude, longitude)).parent(12).id()
table.put_item(
Item={
'CellID': str(cell_id),
'Latitude': latitude,
'Longitude': longitude,
'StoreName': 'Sample Store',
'StoreType': 'Cafe'
}
)
4. dynamodb-geo가 필요할까?
사실 dynamodb-geo는 반경 쿼리(radius query)와 상자 쿼리(bounding box query) 등을 쉽게 할 수 있도록 만들어진 라이브러리입니다. 하지만, 이미 S2 셀 ID나 H3 셀 ID를 사용해 커버링 셀을 미리 계산해 DynamoDB에 저장해둔다면 dynamodb-geo가 꼭 필요하지는 않습니다.
- 반경 쿼리 (Radius Query): 사용자가 특정 지점(위도, 경도)으로부터 일정 반경 내에 있는 데이터를 조회하는 방법입니다.
예를 들어, 사용자가 현재 위치에서 반경 10km 내의 모든 매장 정보를 조회하고자 할 때 사용됩니다. - 상자 쿼리 (Bounding Box Query): 특정 범위 내의 데이터를 조회하는 방식으로, 사각형 형태의 영역을 정의하여 해당 영역 안에 있는 모든 데이터를 찾습니다.
예를 들어, 사용자가 서울 지역에 있는 모든 매장을 찾고 싶을 때, 서울의 경계값을 설정하여 그 안에 포함된 매장 데이터를 조회할 수 있습니다. - 커버링 셀 (Covering Cell): 특정 지역을 커버하는 셀들을 계산하는 방법입니다.
예를 들어, 하나의 지역을 여러 개의 작은 셀로 나누어 각 셀에 ID를 부여하고, 이를 바탕으로 해당 지역에 속한 데이터를 조회할 수 있습니다.
dynamodb-geo와 자체 S2 셀 계산의 차이점
- dynamodb-geo는 반경이나 범위를 자동 계산하여 커버링 셀을 관리하지만, 최신화가 중단된 상태입니다.
- 반면, 자체적으로 S2나 H3 셀을 계산하면 최신 버전의 라이브러리 업데이트가 가능하며, 필요에 따라 다양한 해상도를 사용할 수 있습니다.
5. H3를 쓸 때와 S2를 쓸 때의 차이점
H3와 S2 모두 효율적으로 셀 ID를 계산할 수 있지만, 그 목적과 최적화 요소가 다릅니다.
- H3는 주로 육각형 그리드를 사용해 보다 균일한 셀 크기를 제공하며, 범위나 해안선에 따라 다각형을 다루는 데 효과적입니다.
- S2는 경도에 따른 셀 왜곡이 있지만, 구형 셀 체계이므로 위경도를 더욱 직관적으로 다룰 수 있습니다. DynamoDB의 지리 데이터 인덱싱에 효과적이며, 다양한 해상도에서 사용이 가능합니다.
결론
S2cell과 H3가 무엇인지 알아보았습니다. 사실 아직 감이 잘 안잡혀서
이후에는 정확한 활용 예시나 왜 H3로 마이그레이션하려고 하는지에 대해서 다뤄보고자 한다.