해당 포스팅은 사이드 프로젝트 진행 중 겪은 크고 작은 이슈들에 대한 기록입니다.
읽기에 앞서
- 아직 QueryDSL Spatial의 내부 동작까지 이해한 상태가 아니라 깊은 내용은 없습니다. 단지 프로젝트에 적용하면서 발생한 에러들과 그것을 해결한 과정, 그리고 프로젝트에 필요한 부분을 성공적으로 QueryDSL로 이식시킨 내용을 기록하였습니다.
QueryDSL로 변경하기
- 앞서 구현한 모든 기능들은 JPQL + Geolatte 조합이었다.
- 현재 진행하는 프로젝트에선 QueryDSL을 사용 중이고, 위의 조합으론 QueryDSL의 장점을 만끽하지 못하기에 결국 QueryDSL용 Spatial에 대해서 공부하기로 다짐하였다.
QueryDSL의 장점을 간단하게 알고 가자
- JPQL은 문자로 쿼리를 작성하지만 QueryDSL은 java 코드로 쿼리를 작성하기 때문에 컴파일 시점에 오류를 쉽게 발견할 수 있음
- 또한 java 코드이기 때문에 IDE의 자동 완성 기능을 도움받을 수 있음
- 동적 쿼리 작성에 상당히 편리함 (주어지는 조건을 메서드로 추출하여 재사용성 또한 증가시킬 수 있음)
- Projection을 사용하는 경우 (DTO 조회) JPQL에서처럼 패키지 경로를 모두 적어주는 번거로운 과정을 겪지 않아도 됨
QueryDSL Spatial 환경 설정
-
QueryDSL Spatial 역시 별도의 Dependency를 추가해줘야 한다.
-
아래 트리를 보면 Hibernate Spatial과 마찬가지로 Geolatte와 jts가 같이 설치되는 것을 확인할 수 있다.
-
우리는 이 중 빨간색 원으로 표시한 querydsl-spatial 라이브러리만을 사용할 것이다.
-
querydsl-sql-spatial과 querydsl-sql가 없어도 필요한 기능은 일단 구현할 수 있었기에 이 두 라이브러리는 일단 배제하였다. (둘에 대한 정보를 찾는 것이 너무 힘듬 -> 추후에 공부하게 된다면 사용할 수 있으므로 굳이 제거하진 않았음)
querydsl-spatial 라이브러리 훑어보기
- 위의 사진을 보면 총 3가지의 마커로 표시를 해놓은 것을 확인할 수 있는데, 패키지에 있는 메소드들 마다 사용하는 Geometry 라이브러리가 다르다.
- jts
com.vividsolutions.jts.geom
사용 (얘는 같이 설치조차 안 됨)
- locationtech
org.locationtech.jts.geom
사용
- 바깥의 나머지 쭉
- 앞서 보았듯이 것처럼 우린 Geolatte를 쓰고 있으므로 바깥쪽에 있는 기능들 즉, com.querydsl.spatial 경로에 있는 기능들만 사용하면 되며, 그 중 빨간 물결로 표시해둔 GeometryExpressions가 핵심 기능이다.
GeometryExpressions.asGeometry 사용하기
- 과정은 아래와 같다.
- GeometryExpressions의 메소드 중 asGeometry(Geometry)를 사용하여 복잡한 내부 과정을 거침
- 위 과정으로 반환된 객체 (추상 클래스 GeometryExpression를 상속함)에 정의된 메소드를 활용
- 최종적으로 QueryDSL에서 동적 쿼리 조건에 사용하는 BooleanExpresion으로 반환됨
- asGeometry 메소드는 제네릭 메소드로 Geometry를 받기로 명시되어 있다.
- 예제 코드
private BooleanExpression pointContains(Geometry<G2D> polygon) {
return GeometryExpressions
.asGeometry(polygon)
.contains(entity.point);
}
주의점 1
- 엔티티에 Spatial Type을 가진 컬럼이 있어야한다.
- 만약 엔티티의 좌표가 Point가 아닌 Lat, Lon로 구분된 형식으로 되어있으면 위 메소드들의 파라미터에 들어오는 Geometry 객체를 올바르게 생성할 수 없다.
private BooleanExpression pointContains(Geometry<G2D> polygon) {
return GeometryExpressions
.asGeometry(polygon)
.contains(Wkt.fromWkt("Point(" + entity.lat + " " + entity.lon ")");
}
- 위처럼 사용하면 아래의 에러가 발생한다. Wkt Decoder가 fromWkt에 주어진 String값을 디코딩 못해서 발생하는 에러인듯 하다.
- 처음 이 기능을 사용한 엔티티에 Point 컬럼이 따로 없이 Lat Lon만 가지고 있었기에 위의 예시처럼 사용했다가 발생한 에러하였다.
- 이유를 유추해보자면 디코딩하는 시점에 entity.lat, entity.lon의 실제값은 NumberPath 클래스를 가지고 있다. 이는 QueryDSL만의 특수한 클래스로 (타고 타고 올라가면 DslExpression 클래스의 자식) 내부 로직이 어떤식으로 동작하는지 아직은 잘 모르지만 확실한건 JPAQueryFactory로 생성한 쿼리가 실행되는 시점에서 Q타입 엔티티의 값을 가져다 쓰는 것이 일반적인 방법으론 불가능하다는 것이다. (방법이 있다면 가르쳐주세요!)
- 실제로 값을 출력해보면 아래와 같이 나온다.
- 즉, Wkt Decoder는 String으로 주어진 WKT를 토대로 Geometry를 만들어내는 것인데 Lat, Lon 자리에 숫자가 아닌 엉뚱한 값이 들어가서 에러가 발생하는 것이다.
- 이 방법 저 방법 다 사용해봤으나 결국 나도 Point 컬럼을 추가하는 방법을 선택하였고 문제를 해결하였다. (사실 처음부터 그랬으면 겪지도 않았을 에러였겠지만 덕분에 이렇게 해선 안 된다는 것을 배웠고 이를 해결하고 공부가 되어 더 뿌듯하고 만족도도 높았다.)
주의점 2
- 현재 5.0.0 버전에서 MBRContains를 지원하지 않는 것 같다.
- GeometryExpression 추상 클래스에 들어가보면 정말 많은 기능들을 지원하는데 MBRContains는 없다.
- 따라서 MBRContains를 굳이 써야겠다면 JPQL을 사용해야 한다.
- 신기한 건 이전 포스팅에서 언급했던 것처럼 MBRContains는 Hibernate Spatial에서도 지원한다는 내용이 명시되어 있지 않은데 JPQL을 이용하면 작동을 하였다. 그렇기 때문에 QueryDSL Spatial에도 방법이 있지 않을까 싶어서 찾아봤으나 이쪽은 정말 정보가 없어도 너무 없었고, 꼭 MBRContains를 써야하는 것도 아니기에 다음으로 미뤄두었다.
결론
- 현재까지 QueryDSL Spatial이 Hibernate Spatial을 대체하지 못한 부분은 경험하지 못하였고, 성능적인 저하 또한 없습니다. 따라서 QueryDSL의 도입을 고려하시거나 또는 벌써 QueryDSL의 장점을 충분히 만끽하고 계신 분들이라면 QueryDSL Spatial 또한 가볍게 사용해보시는 것도 좋을 것 같습니다!