[Springboot] PostGIS를 이용한 좌표 간 거리 계산

일단 해볼게·2023년 2월 28일
0

Springboot

목록 보기
8/26

데이터베이스에 위도와 경도가 있는 상황에서 반경 1.5km 내의 게시글 찾는 API를 구현하려한다. 그러다 PostgreSQL에서 지원하는 함수를 찾아보니 earthdistancePostGIS를 알게 됐다.

earthdistance는 postgreSQL에서 공식적으로 지원하는 모듈인 대신, 지구가 타원형이 아닌 완벽한 구의 형태라고 가정하고 사용하는 모듈이라 정밀한 거리 계산에 맞지 않다고 생각했다.

PostGIS의 함수 중 ST_DWithinuse_spheroid 옵션으로 지구가 타원형인지 완벽한 구의 형태라고 지정할 수 있는 옵션이 있어 ST_DWithin를 채택했다.
정밀한 계산을 원한다면 use_spheroid = true, 빠른 속도를 원한다면 use_spheroid = false 옵션을 적용하면 된다.

공식문서 : https://postgis.net/docs/ST_DWithin.html

환경설정

postgresql이 springboot에 적용됐다는 가정하에 진행한다. 필자는 로컬 환경이 아닌 docker-compose 환경에서 postgresql을 사용하고 있다.

  1. build.gradle의 dependency에서 hibernate-spatial을 설치한다.
runtimeOnly 'org.postgresql:postgresql'
implementation 'org.hibernate:hibernate-spatial:5.6.11.Final'
  1. application.yml 설정
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
  1. Dockerfile 설정
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로 직접 설치했다.

  1. docker-compose.yml 설정
  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 에서 참고했다.

PostRepository

@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은 거리를 뜻한다. 단위는 미터이다.

PostService

@Transactional(readOnly = true)
  public List<PostInfo> getPostInDistance(Float userLatitude, Float userLongitude) {
    return postRepository.findPostsByDistance(userLatitude, userLongitude).stream()
        .map(this::mapPostEntityToPostInfo)
        .collect(Collectors.toList());
  }

조건에 맞는 게시글을 추출하고 stream을 이용해 리스트로 형변환했다.

PostController

@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
      }
    }
  ]
}

모든 코드 : https://github.com/TecheerBootios/RUAlone-Backend

profile
시도하고 More Do하는 백엔드 개발자입니다.

0개의 댓글