본문 바로가기

Querydsl

[Querydsl] MySQL 공간 데이터(Point)의 반경 검색 (ST_CONTAINS)

이번 포스팅에선 숙소 예약 프로젝트를 진행하며 Querydsl로 공간 데이터를 조건으로하는 표현식(Expression)에 대해 알아보겠습니다.

기본 환경 및 의존성

기본 환경과 의존성은 다음과 같습니다.

- Java 17

- Spring Boot 3.3.0

- DB: MySQL 8.0

- ORM: querydsl 5.1.0

- 공간 데이터 의존성: hibernate-spatial 6.5.2

Entity

@Entity
public class Stay {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "STAY_ID")
    private Long id;

    @Column(name = "POINT", columnDefinition = "POINT SRID 4326", nullable = false)
    private Point point;

    // 그 외 필드들...
}
  • MySQL에선 공간 데이터를 Point 라는 타입으로 지원합니다.
  • ORM에서 공간 데이터임을 알려주기 위해 `hibernate-spatial` 에서 지원하는 `Point 타입`으로 `위도`와 `경도`를 표현해야하며, 이를 위해 데이터베이스의 컬럼 속성으로 `SRID 4326` 을 지정해야 합니다. (설정하지 않으면 SRID 값은 0 이 되며 이는 `위도`, `경도`가 아닌 `직교좌표`로 표현됩니다.)
  • 만약 처음부터 `SRID 4326` 으로 설정되지 않았다면 컬럼을 수정할 수 없으니 주의해야 합니다.
  • (포스팅과 상관없는 내용이지만, 추후 공간 인덱스(R-Tree)를 적용하기 위해 `Not Null` 조건이 되야하므로 `nullable=false` 도 추가했습니다.)

공간 데이터를 검색하는 방법

MySQL에서 공간 데이터를 검색하는 방법은 MySQL에서 지원하는 공간 함수를 이용하는 것입니다.

이번 포스팅에서 사용하는 공간 함수는 다음과 같습니다.

  • `ST_CONTAINS(g1, g2)` : g1(지오메트리)가 g2(지오메트리)를 완전히 포함하면 `true`, 그렇지 않으면 `false` 를 반환
  • `ST_BUFFER(중심점, 반지름(단위: 미터))` : 전달받은 `중심점(지오메트리)`을 기준으로 반지름 크기(단위: 미터)만큼의 `원형`을 그리는 함수
  • `ST_GeomFromText(WKT)` : 텍스트 문자열(`WKT`)로 지오메트리 데이터를 생성하는 함수
    • `WKT(Well-Known-Text)` 포맷 : `POINT(35.000000, 127.000000)` 과 같은 지오메트리 형식으로 작성된 포맷을 말합니다.

 

이 함수들을 이용해 SQL 의 WHERE 조건문으로 다음과 같이 표현할 수 있습니다.

SQL WHERE 문

WHERE ST_CONTAINS(ST_BUFFER(ST_GeomFromText('POINT(35, 127)', 4326), 5000), `Point_타입_컬럼_이름`)

조건문의 가장 내부부터 의미를 하나씩 보면,

1. `ST_GeomFromText('POINT(35, 127)', 4326)` : `SRID 4326` 의 위도와 경도를 갖는 `중심점`을 생성합니다.

2. `ST_BUFFER(중심점, 5000)` : 위의 좌표를 중심으로 반지름이 5km (=5000m)인 `원형`을 생성합니다.

3. `ST_CONTAINS(원형, Point_타입_컬럼_이름)` : `생성된 원형`에 `완전히 포함`되는 `Point 타입의 컬럼`의 레코드들을 조건으로 찾습니다.

 

이제 SQL 문을 만들기 위해 Querydsl 로 어떻게 작성해야 할까요?

Querydsl 표현식 만들기

먼저, Querydsl 로 작성한 전체적인 쿼리는 다음과 같습니다.

List<Stay> stays = jpaQueryFactory
                .select(stay)
                .from(stay)
                .where(
                  // 조건문
                )
                .fetch();

querydsl 에 where 조건문으로 사용하기 위해선 `BooleanExpression` 을 사용할 수 있습니다.

원하는 공간 함수 표현을 `BooleanExpression` 으로 표현하는게 먼저겠죠?

저는 다음과 같이 `BooleanExpression` 을 반환하는 함수를 만들었습니다.

private BooleanTemplate getContainsBooleanExpression(Double latitude, Double longitude, Integer radius) {
    String target = "Point(%f %f)".formatted(latitude, longitude);
    String geoFunction = "ST_CONTAINS(ST_BUFFER(ST_GeomFromText('%s', 4326), {0}), point)";
    String expression = String.format(geoFunction, target);

    return Expressions.booleanTemplate(expression, radius);
}

이 함수에서 눈여겨 봐야할 곳은, 중심점을 만드는 검색 대상의 좌표(`target`)입니다.

 

SQL 에선 Point(latitude, longitude) 와 같이 위도와 경도 사이에 쉼표(,)가 들어가지만, querydsl 표현식에선 사용하지 않습니다.

 

추가로, 변수 `geoFunction` 부분에서 {0} 부분에 들어가는건 `radius` 이며, `point`는 데이터베이스 테이블의 `Point 타입(SRID 4326)`으로 좌표 정보에 해당하는 컬럼 이름입니다.

 

이렇게 반환된 `BooleanExpression`을 where() 내부에 사용하시면 중심 좌표를 기준으로 `반지름 x 미터에 있는 숙소`를 찾을 수 있습니다.

 

공간 인덱스가 있다면 더욱 빠르게 찾을 수도 있으니 아래 사이트를 참고하시면 도움이 될 것 같습니다.

 

이번 포스팅은 여기서 마치겠습니다.

도움이 되셨길 바라며 읽어주셔서 감사합니다.

참고

테코블 포스팅 : 공간 데이터 개념부터 적용까지