데이터베이스에 위도와 경도가 있는 상황에서 반경 1.5km 내의 게시글 찾는 API를 구현하려한다. 그러다 PostgreSQL에서 지원하는 함수를 찾아보니 earthdistance
와 PostGIS
를 알게 됐다.
earthdistance
는 postgreSQL에서 공식적으로 지원하는 모듈인 대신, 지구가 타원형이 아닌 완벽한 구의 형태라고 가정하고 사용하는 모듈이라 정밀한 거리 계산에 맞지 않다고 생각했다.
PostGIS
의 함수 중 ST_DWithin
의 use_spheroid
옵션으로 지구가 타원형인지 완벽한 구의 형태라고 지정할 수 있는 옵션이 있어 ST_DWithin를 채택했다.
정밀한 계산을 원한다면 use_spheroid = true, 빠른 속도를 원한다면 use_spheroid = false 옵션을 적용하면 된다.
공식문서 : https://postgis.net/docs/ST_DWithin.html
postgresql이 springboot에 적용됐다는 가정하에 진행한다. 필자는 로컬 환경이 아닌 docker-compose 환경에서 postgresql을 사용하고 있다.
runtimeOnly 'org.postgresql:postgresql'
implementation 'org.hibernate:hibernate-spatial:5.6.11.Final'
spring:
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/rualone
username: DB name
password: DB password
jpa:
# 중요
database-platform: org.hibernate.spatial.dialect.postgis.PostgisPG95Dialect
show-sql: true
generate-ddl: true
open-in-view: false
FROM postgres:14.1
RUN apt-get update && apt-get install -y postgresql-14-postgis-3
CMD ["/usr/local/bin/docker-entrypoint.sh","postgres"]
도커 컨테이너 내부에서 postgis를 설치하려다가 superuser로 접속하는 구간에서 막혀 Dockerfile로 직접 설치했다.
postgresql:
container_name: postgresql
build:
context: ./src # 필요 - dockerfile 경로
dockerfile: Dockerfile # 필요 - dockerfile 이름
image: postgres:13.2
expose:
- "5432"
tty: true
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_HOST_AUTH_METHOD: "trust"
volumes:
- ./src:/docker-entrypoint-initdb.d/ # 필요
ports:
- "5432:5432"
3, 4는 https://stackoverflow.com/questions/66205577/how-do-i-add-postgis-to-my-postgresql-setup-using-pure-docker-compose 에서 참고했다.
@Query(
value =
"select * from Post p join Location l on p.id = l.post_id where"
+ " ST_DWithin(CAST(ST_SetSRID(ST_Point(:userLatitude, :userLongitude), 4326) AS"
+ " geography), CAST(ST_SetSRID(ST_Point(l.latitude, l.longitude), 4326) AS"
+ " geography), 1500) and p.is_active is true",
nativeQuery = true)
List<Post> findPostsByDistance(
@Param("userLatitude") float userLatitude, @Param("userLongitude") float userLongitude);
}
좌표 간 거리에 따른 게시글을 조회하기 위해 Post와 Location을 join했다.
ST_DWithin(geography gg1, geography gg2, double precision distance_meters, boolean use_spheroid = true)
ST_DWithin 함수에서 geography 객체가 필요하기 때문에
위도, 경도 -> Point객체 -> geography객체로 변경해서 함수에 맞게 형변환했다.
CAST() AS 대신
geography::STGeomFromText('POLYGON((-122.358 47.653 , -122.348 47.649, -122.348 47.658, -122.358 47.658, -122.358 47.653))', 4326))
geography:: 으로 타입캐스팅하려다가 :은 JPQL에서 파라미터를 지칭하기 때문에 에러가 발생했다.
ST_DWithin의 마지막 인자인 1500은 거리를 뜻한다. 단위는 미터이다.
@Transactional(readOnly = true)
public List<PostInfo> getPostInDistance(Float userLatitude, Float userLongitude) {
return postRepository.findPostsByDistance(userLatitude, userLongitude).stream()
.map(this::mapPostEntityToPostInfo)
.collect(Collectors.toList());
}
조건에 맞는 게시글을 추출하고 stream을 이용해 리스트로 형변환했다.
@GetMapping("/api/post/list/distance")
public ResponseEntity<ListResult> getPostListInDistance(
@RequestParam Float userLatitude, @RequestParam Float userLongitude) {
List<PostInfo> postInfoList = postService.getPostInDistance(userLatitude, userLongitude);
return ResponseEntity.ok(responseService.getListResult(postInfoList));
}
{
"success": true,
"code": 0,
"msg": "성공하였습니다.",
"list": [
{
"title": "string",
"chatUrl": "string",
"creatorName": "하니",
"startAt": "2023-03-28T18:08:52.511",
"limitMember": 3,
"place": "string",
"foodCategory": "한식",
"postType": "배달",
"locationInfo": {
"latitude": 37.339912,
"longitude": 37.339912
}
},
{
"title": "string",
"chatUrl": "string",
"creatorName": "윈터",
"startAt": "2023-03-28T18:08:52.511",
"limitMember": 3,
"place": "string",
"foodCategory": "한식",
"postType": "배달",
"locationInfo": {
"latitude": 37.339912,
"longitude": 37.339912
}
}
]
}