[PostgreSQL] PostGIS 설치 및 사용 - 컴도리돌이
주어진 좌표가 어떤 행정구역에 포함되는지 알아야 하는 상황이 생겼습니다. 여러 방법이 있지만 그중에서 PostgreSQL에 있는 확장 라이브러리인 PostGIS가 속도 및 정확성에 성능이 매우 뛰어난다
comdolidol-i.tistory.com
[PostgreSQL] PostGIS 성능 비교: GEOMETRY vs TEXT 저장 방식, 얼마나 차이 날까? - 컴도리돌이
회사에서 PostGIS 도입을 고민하면서, 어떤 이슈가 발생할지 모르기 때문에 행정구역 경계 값을 TEXT로 처리하여 JSON 형태로 변환 후 Elasticsearch의 geo-bounding box query로 조회하는 방안을 계획했습니다
comdolidol-i.tistory.com
운영 데이터에서 특정 행정구역의 경계 내에 몇 개의 데이터가 존재하는지 확인해야 하는 경우가 많습니다. 이를 위해 PostgreSQL의 확장 기능인 PostGIS를 고려했지만, 실제 운영 환경에서 발생할 수 있는 성능 이슈를 예측하기 어려웠습니다. 특히 대량의 공간 데이터를 실시간으로 조회할 때, RDBMS보다 Elasticsearch가 더 빠르고 효율적일 것으로 판단하여 이를 활용하기로 했습니다. 이번 포스팅에서는 Elasticsearch를 이용하여 경계 데이터를 저장하고, 해당 경계 내에 존재하는 데이터를 집계하는 방법을 소개하겠습니다. 🤓
Elasticsearch에서 공간 데이터를 다룰 때, 일반적으로 geo_shape 필드를 사용하여 공간 검색을 수행할 수 있습니다. 하지만 저는 보다 유연한 처리와 빠른 인덱싱을 위해 WKT(Well-Known Text) 값을 text 형식으로 저장한 후, Java 로직에서 이를 변환하여 조회하는 방식을 채택했습니다. 이 방법을 사용하면 Elasticsearch의 기본 기능에 의존하지 않고도 공간 데이터를 효율적으로 다룰 수 있습니다.
먼저, 경계 데이터를 저장할 Elasticsearch 인덱스를 생성합니다. name 필드는 행정구역명을 저장하며, wkt 필드는 WKT 형식으로 경계 정보를 저장하는 역할을 합니다.
PUT boundaries
{
"mappings": {
"properties": {
"name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"wkt": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}
위 설정을 통해 행정구역의 이름과 WKT 데이터를 저장할 수 있도록 했습니다. 이후 데이터를 추가하는 과정에서 길이가 길어 일부 생략하였지만, 예제로 서울특별시의 경계 데이터를 다음과 같이 추가할 수 있습니다.
POST boundaries/_doc/1
{
"name": "서울특별시",
"wkt": "MULTIPOLYGON(((26.82434476795297 37.51022586624618,126.82412736794552 37.5104182450281,126.82406008111737 37.510480817253125,126.82385670190524 37.51087508322444,126.82414707105036 37.512440442257486,126.82440812510971 37.51317655114924,129.38753070286643)))"
}
경계 내 포함 여부를 검사할 데이터를 저장하기 위해 새로운 인덱스를 생성했습니다. 여기서는 geo_point 필드를 사용하여 위치 데이터를 저장하며, name 필드는 장소명을 의미합니다.
PUT locations
{
"mappings": {
"properties": {
"name": { "type": "keyword" },
"location": { "type": "geo_point" }
}
}
}
테스트 데이터를 추가하여 특정 위치 정보를 저장했습니다.
POST locations/_doc/1
{
"name": "서울특별시 강남구 역삼동 638-12번지",
"location": { "lat": 37.502127203, "lon": 127.033051782 }
}
이제 Elasticsearch의 geo_shape 필터를 활용하여 특정 행정구역 내 포함된 데이터를 조회할 수 있습니다. 다음 쿼리는 다각형 좌표를 기준으로 특정 지역 내부에 포함된 데이터를 검색합니다.
GET locations/_search
{
"query": {
"bool": {
"filter": {
"geo_shape": {
"location": {
"shape": {
"type": "polygon",
"coordinates": [[[127.0276, 37.4979], [127.0353, 37.4969], [127.0378, 37.5010], [127.0298, 37.5042], [127.0276, 37.4979]]]
},
"relation": "within"
}
}
}
}
}
}
위의 쿼리는 강남구 경계 내에 포함된 모든 데이터를 검색합니다. 여기서 relation 옵션을 변경하면 경계 내부(within), 경계 선(intersects), 특정 지점(contains)에 따라 필터링할 수 있습니다. 데이터를 집계하여 해당 영역 내 개수를 구하려면 value_count 집계를 사용할 수 있습니다.
GET locations/_search
{
"size": 0,
"query": {
"bool": {
"filter": {
"geo_shape": {
"location": {
"shape": {
"type": "polygon",
"coordinates": [[[127.0276, 37.4979], [127.0353, 37.4969], [127.0378, 37.5010], [127.0298, 37.5042], [127.0276, 37.4979]]]
},
"relation": "within"
}
}
}
}
},
size: 1,
"aggs": {
"count_within_boundary": {
"value_count": { "field": "name" }
}
}
}
이제 Java 코드에서 WKT 값을 조회하고 이를 활용하여 특정 경계 내 데이터를 필터링하는 로직을 구현할 수 있습니다.
public Optional<String> getWktFromSiteBounds() throws IOException {
SearchRequest searchRequest = new SearchRequest.Builder()
.index("boundaries")
.build();
return elasticsearchClient.search(searchRequest, Map.class).hits().hits()
.stream()
.map(Hit::source) // JsonData 대신 Map으로 처리
.filter(Objects::nonNull)
.map(source -> (String) source.get("boundary")).findFirst();
}
위의 메서드는 boundaries 인덱스에서 WKT 값을 조회하며, 조회된 데이터가 없을 경우 빈 값을 반환합니다. 이후 WKT 값을 활용하여 geo_shape 필터를 적용하여 경계 내 데이터를 집계하는 로직을 작성할 수 있습니다.
public int countLocationsWithinBounds() throws IOException {
Optional<String> wktOptional = getWktFromSiteBounds();
if (wktOptional.isEmpty()) { return 0; }
String wkt = wktOptional.get();
return (int) elasticsearchClient.count(
c -> c.index("locations")
.query(q -> q.geoShape(
gs -> gs
.field("location")
.shape(
s -> s
.shape(JsonData.of(wkt))
.relation(GeoShapeRelation.Within)
)
))
).count();
}
이 메서드는 count API를 활용하여 특정 경계 내에 포함된 데이터를 집계하며, GeoShapeRelation.Within 옵션을 사용하여 해당 지역 내부에 있는 데이터 개수를 반환합니다. 이를 통해 Elasticsearch에서 공간 데이터를 효과적으로 활용하여 특정 행정구역 내 데이터를 집계하는 방법을 정리했습니다. Elasticsearch의 geo_shape 기능을 활용하면 공간 데이터를 효율적으로 처리할 수 있으며, Java 코드와 함께 적용하면 보다 유연한 공간 데이터 검색 및 집계가 가능합니다.