이번 포스팅에선 숙소 예약 프로젝트를 진행하며 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 미터에 있는 숙소`를 찾을 수 있습니다.
공간 인덱스가 있다면 더욱 빠르게 찾을 수도 있으니 아래 사이트를 참고하시면 도움이 될 것 같습니다.
이번 포스팅은 여기서 마치겠습니다.
도움이 되셨길 바라며 읽어주셔서 감사합니다.
참고
테코블 포스팅 : 공간 데이터 개념부터 적용까지