Geo Django(DRF), DB Extension(PostGIS)으로 WAS를 확장하고 지리데이터를 사용하는 방법을 알아보고자 한다
네이버나 카카오 맵을 보면 건물과 땅이 표시되어있다
이런 것도 전부 데이터로 받아오는 것일텐데, 한번쯤 생각해본적은 있으나 그냥 지나치곤했다
QGIS와 Jupyter(python) 으로 SHP파일 다뤄보기
우선 shp 파일을 구한다(오픈데이터로 공개된 것들이 있어 찾는데 어렵지 않았다)
shp 파일을 geojson 으로 변환하고, api 로 사용할 수 있도록 하는 것이 1차적인 목표였다
shp 파일을 처음 봤고, 이게 무엇인지 알아보기위한 과정을 거친다
- jupyter로 Python 환경에서 shp → geojson 변환
- geopandas 라이브러리로 데이터 확인; col 이름과 row값 어떤식인지 가볍게 확인한다
import geopandas as gpd
data_from_shp = gpd.read_file(r'{file_path}.shp',encoding='utf-8')
QGIS 프로그램을 활용해 shp 파일이 어떤 데이터인지 직관적으로 확인한다
QGIS 로 shp 파일을 열었을 때 다음과 같이 나온다이제 직접 데이터를 쿼리해보는 과정을 통해 검증해야한다
- 지역이 잘 구분되어져있는 어떤 모양으로 나오는 것을 보니, 파일이 정상임을 확인했으나 이렇게만 해서는 어디가 어디인지 알 수 가 없어, 내가 위치 필터링을 하고싶다고 해도 그게 맞는것인지 알 수 가없다.
이제 직접 데이터를 쿼리해보는 과정을 통해 검증해야한다
OS(M1) 라이브러리 설치
먼저 Mac Os 환경을 기준으로 아래 라이브러리를 설치해준다
brew install geos # 지오메트리 연산수행(겹치거나 포함)
brew install gdal # 지리데이터형식
brew install proj # 좌표계변환(안함)
OS 레벨의 라이브러리를 설치하는 이유는 지리적연산이 백엔드 애플리케이션 전반적으로 필요하기 때문이다.
짚고 넘어가면
- Database
- PostgreSQL
- PostGIS확장 설치
- PostgreSQL
- Python Library
- Django ORM Level
Django Setting
pip install djangorestframework-gis
installed_app 에 django.contrib.gis 추가
GDAL_LIBRARY_PATH = "/opt/homebrew/lib/libgdal.dylib" # M1 Mac 예시
GEOS_LIBRARY_PATH = "/opt/homebrew/lib/libgeos_c.dylib" # M1 Mac 예시
postgresql 추가됨
DATABASES = {
"default": {
"ENGINE": "django.contrib.gis.db.backends.postgis",
"NAME": -,
"USER": -,
"PASSWORD": -,
"HOST": -,
"PORT": "5432",
}
}
DATABASES = {
'default': {
'ENGINE': 'django.contrib.gis.db.backends.postgis',
# 나머지 설정
},
}
여기서 Django 단에 대한 세팅은 끝났다
그리고 아래와 같은 에러를 만나게 된다
"extension "postgis" is not available"
django.db.utils.NotSupportedError: extension "postgis" is not available
DETAIL: Could not open extension control file "/usr/share/postgresql/15/extension/postgis.control": No such file or directory.
HINT: The extension must first be installed on the system where PostgreSQL is running.
extension 을 설치하라고 친절한 힌트가 제공된다.
PostGIS 확장하기
PostgreSQL DB를 생성 후, psql로 접속하여 확장한다
(PostGIS는 점(Point), 선(LineString), 폴리곤(Polygon), 멀티폴리곤(MultiPolygon) 등 다양한 지리 공간 데이터 타입을 지원한다 데이터를 확인했을 때 지리에 관한 데이터 양식에 MultiPolygon 이라고 되어있었기 때문에 이 것을 모델의 필드로 적용했다)
postgis 확장방법은
https://postgis.net/docs/manual-2.4/postgis-ko_KR.html#create_new_db_extensions
문서에 친절하게 나와있다
CREATE EXTENSION postgis;
기존에 로컬에서 DB를 도커 컨테이너로 사용한다.
때문에 컨테이너에 postgis 설치함
docker exec -it {container-name} bash
apt-get update
apt-get install -y postgis
psql -U {postgres-user} -d boda_local_db -c "CREATE EXTENSION postgis;"
model, serializer 예시
여기서 geometry 필드가 중요한데, 이걸 기준으로 쿼리요청을 한다
from django.contrib.gis.db import models
class GeoFeature(models.Model):
name = models.CharField(max_length=100)
geometry = models.MultiPolygonField()
from rest_framework_gis.serializers import GeoFeatureModelSerializer
from .models import GeoFeature
class GeoFeatureSerializer(GeoFeatureModelSerializer):
class Meta:
model = GeoFeature
fields = ('id', 'name', 'geometry')
DB에 데이터 적재
원래shp 파일의 데이터를 그대로 사용하려했으나,
QGIS에서 shp 파일을 geojson으로 변환했다
geojson으로 파일을 ORM 으로 마이그레이션하는 예시코드
(참고 : 실제 프로젝트에서 사용한 필드 값은 혹시 몰라 다르게 예시를 작성한다)
import geopandas as gpd
from django.contrib.gis.geos import GEOSGeometry
from app.geo_feature.models import GeoFeature
gdf = gpd.read_file("file.geojson")
def initial_geo_data():
for index, row in gdf.iterrows():
# Shapely 객체를 GEOSGeometry 객체로 변환
geos_geom = GEOSGeometry(row["geometry"].wkt)
feature = GeoFeature(
name=row["name"],
geometry=geos_geom, # 변환된 GEOSGeometry 객체를 할당
)
feature.save()
데이터가 변환되었고
아래 ORM으로 데이터를 잘 쿼리했다.
x,y 좌표를 기준으로 1km 반경 안에 있는 queryset을 가져온다
x = 경도
y = 위도
distance = 1 # 1km
p = Point(x, y, srid=4326)
queryset = queryset.annotate(distance=Distance("geometry", p)).filter(distance__lt=D(km=distance))
여기서 srid=4326 는 위도와 경도를 사용한 좌표계이다
SQL 문(1000이면 1km)
SELECT * FROM 테이블명 WHERE ST_DWithin(geometry, ST_SetSRID(ST_Point(x, y), 4326)::geography, 1000);
- ST_DWithin(A, B, distance): 두 지리 공간 객체 A와 B가 주어진 distance 이내에 있는지 확인하고, 함수는 인덱스를 활용해 빠른 검색이 가능하다
- ST_SetSRID(ST_Point(x, y), srid): **ST_Point**로 생성된 점에 공간 참조 ID (SRID)를 설정한다(여기서는 WGS 84 좌표계 - SRID 4326)
- ::geography: geometry 타입을 geography 타입으로 변환. geography 타입은 좌표를 스피로이드에 투영하여 더 정확한 거리와 면적 계산을 가능하게 한다 (스피로이드는 3차원 타원 구체도형이다; 지구와 유사한 도형으로 세팅해서 계산을 정확하게 도와주는 역할을 한다)
ORM으로 x, y 에 따라 Query 하기
특정 x, y, distance 를 받아 필터링을 해야한다
(파라미터를 받기위한 FilterSet을 View단에서 filter_class로 지정했다)
import django_filters
from django.contrib.gis.db.models.functions import Distance
from django.contrib.gis.geos import Point
from django.contrib.gis.measure import D
from rest_framework.exceptions import ValidationError
from app.geo_feature.models import GeoFeature
class GeoFeatureFilter(django_filters.FilterSet):
x = django_filters.NumberFilter(method="filter_by_distance")
y = django_filters.NumberFilter(method="filter_by_distance")
distance = django_filters.NumberFilter(method="filter_by_distance")
class Meta:
model = GeoFeature
fields = []
def filter_by_distance(self, queryset, name, value):
x = self.request.query_params.get("x", None)
y = self.request.query_params.get("y", None)
distance = self.request.query_params.get("distance", None)
if not (x and y and distance):
raise ValidationError("x, y, distance 값은 필수입니다.")
x = float(x)
y = float(y)
distance = float(distance)
p = Point(x, y, srid=4326)
queryset = queryset.annotate(distance=Distance("geometry", p)).filter(distance__lt=D(km=distance))
return queryset
GET으로 받은 결과는 다음처럼 잘 필터되어 나왔다.
예전에 위치기반 데이터 (Geo data)를 활용해 데이터분석 공모전에 참여하면서 사용했었지만,
지금은 DB 와 API를 만드는 존재(?) 가 되었기 때문에 사뭇 데이터를 보는 관점이 달라진 것 같다.
(그때는 데이터를 활용하기 위한 목적으로
코드를 통해
시각화 하고, 원하는 row값을 계산하고
머신러닝 라이브러리를 사용해 유사도를 구하는 정도로만 사용했었다.
백엔드 엔지니어가 된 지금으로선 막상 필요한 데이터를 적재 후 어떻게 데이터를 내보내야 할 것인지 고민하게 되었다)
이제 기술적인 리서치단계이기 때문에
아직 어떤 로직이 더 필요한지, DB에 대한 기능들에 대한 파악도 부족하다
차차 알아보도록 해야겠다
'Develop' 카테고리의 다른 글
노코드 오픈소스 인공지능 자동화툴 N8N 설치하기 (2) | 2024.09.04 |
---|---|
React Icons 알아보기 - 웹 프론트엔드 개발에 사용되는 독보적 라이브러리 (0) | 2024.08.25 |
[AWS] RDS 및 Aurora SSL/TLS C인증서 업데이트 및 자동교체여부 확인하기 (0) | 2023.10.18 |
[Django] refresh_from_db() : 메모리 상의 Django 객체와 데이터베이스 동기화하기 (0) | 2023.10.06 |
[Django/AWS S3] File Chunk Upload | File Multipart Upload로 대용량 파일 업로드 진행상황 확인하고, 효율적으로 업로드하기 (0) | 2023.09.10 |