들어가기
난생 처음으로 오픈소스에 기여를 해보게 됐습니다. 비록 코드가 아닌 문서화 관련이지만 나름 뿌듯한 경험이었고 과정을 기록해놓고 싶습니다.
배경
GitHub - TUK-SPORTIFY/sportify-backend: 2024년 국민체육진흥공단 공공데이터 활용 경진대회 수상작[장려상
2024년 국민체육진흥공단 공공데이터 활용 경진대회 수상작[장려상]. Contribute to TUK-SPORTIFY/sportify-backend development by creating an account on GitHub.
github.com
공모전에 출품한 Sportify 서비스는 위/경도 위치연산을 통해 사용자 주변 위치의 스포츠 이용권 강좌를 제공해야했습니다.
데이터 베이스를 선택할 때 MySQL이 위치연산도 제공하고 있었고 다른 백엔드 팀원과 저 모두 이해도가 높은 MySQL로 결정 했습니다.
ST_DWithin
boolean st_dwithin(Geometry, Geometry, double)
위 함수는 특정 거리안에 Geometry타입이 존재하는 지 boolean값으로 반환하는 함수입니다. 해당 함수를 선택하고자 했지만 MySQL 자체에선 제공하지 않는 함수입니다. (H2, Postgresql, Oracle 등은 지원합니다.)
ST_Buffer, ST_Contains
그래서 선택한 함수는 위 두가지 함수입니다.
Geometry st_buffer(Geometry, double)
double st_distance(Geometry, Geometry)
st_buffer로 특정 Geometry 기준으로 동그란 원을 만들어 Geometry 타입으로 반환합니다.
예를들어 st_buffer(Point(1,1), 1000) 를 호출하면되면 위/경도 1인 지점을 기준으로 1km 원을 만들어 반환합니다.
반환받은 geometry(위 에선 원을 예시로)를 st_distance에 첫 인자로 집어넣고 두 번째 인자에 스포츠 강좌 좌표(Point 타입)를 넣어 원안에 존재하는 지를 질의합니다.
위 두 함수는 MySQL에서 지원하는 함수입니다.
MySQL :: MySQL 8.4 Reference Manual :: 14.16.8 Spatial Operator Functions
14.16.8 Spatial Operator Functions OpenGIS proposes a number of functions that can produce geometries. They are designed to implement spatial operators. These functions support all argument type combinations except those that are inapplicable according to
dev.mysql.com
저희 팀은 JPA를 사용하고 있었고 JPQL로 위 함수를 호출하고 싶었습니다. (물론 Native 쿼리를 날려도 되긴합니다.)
하지만 Hibernate User Guide를 보면 st_buffer를 지원하지 않는 것을 알 수 있습니다.
Hibernate ORM User Guide
Starting in 6.0, Hibernate allows to configure the default semantics of List without @OrderColumn via the hibernate.mapping.default_list_semantics setting. To switch to the more natural LIST semantics with an implicit order-column, set the setting to LIST.
docs.jboss.org
ST_ 접두사
갑자기 ST_ 접두사 이야기를 꺼내는 이유는 SQL에는 ANSI표준이 있듯이 Spatial Query도 SFS라는 표준이 존재하기 때문입니다.
많은 DBMS들은 OpenGIS에서 제정한 SFS표준을 지켜 공간 함수를 정의하고 그 함수의 이름 앞에는 ST_가 붙게 됩니다.
MySQL도 SFS표준을 지켜 함수를 지원하고 있기 때문에 다른 DB들의 Spatial Query를 JPQL로 지원한다면 MySQL도 지원하지 않을 이유가 없는데 위 Hibernate User Guide에는 지원하지 않는다고 설명 돼있습니다.
Hibernate Spatial Query 지원 방식
바로 Hibernate ORM 프로젝트를 포크해 코드를 열어 어떤 방식으로 Spatial 쿼리를 지원하는지 알아봤습니다.
GitHub - hibernate/hibernate-orm: Hibernate's core Object/Relational Mapping functionality
Hibernate's core Object/Relational Mapping functionality - hibernate/hibernate-orm
github.com
java/org/hibernate/spatial/dialect/mysql 패키지로 들어가면 많은 클래스들이 Depreated된 것을 볼 수 있습니다.
Depreated된 MySQLSpatial56Dialect 는 MySQL 5.6.1 버전 이후에 지원하는 방언이지만 문서상에는 아직도 MySQLSpatial56Dialect 를 안내를 하고 있습니다.
MySQL versions before 5.6.1 had only limited support for spatial operators. Most operators only took account of the minimum bounding rectangles (MBR) of the geometries, and not the geometries themselves.
This changed in version 5.6.1, when MySQL introduced ST_* spatial operators. The dialect MySQLSpatial56Dialect uses these newer, more precise operators.
For more information, see this page in the MySQL reference guide (esp. the section Functions That Test Spatial Relations Between Geometry Objects)
MySqlSqmFunctionDescriptors
문서와 달리 현재 Hibernate는 MySqlSqmFunctionDescriptors라는 클래스에서 공간함수 지원 가능 여부를 따지고 있습니다.
public class MySqlSqmFunctionDescriptors extends BaseSqmFunctionDescriptors {
private static final Set<CommonSpatialFunction> UNSUPPORTED = EnumSet.of(
CommonSpatialFunction.ST_BOUNDARY, CommonSpatialFunction.ST_RELATE );
public MySqlSqmFunctionDescriptors(FunctionContributions functionContributions) {
super( functionContributions );
}
@Override
public CommonSpatialFunction[] filter(CommonSpatialFunction[] functions) {
return Arrays.stream( functions )
.filter( f -> !UNSUPPORTED.contains( f ) )
.toArray( CommonSpatialFunction[]::new );
}
}
굉장히 직관적인 코드인데요. UNSUPPORTED라는 EnumSet으로 지원하지 않는 공간 함수를 filter 메서드를 통해 걸러내고 있습니다.
CommonSpatialFunction 내부에 ST_BOUNDARY, ST_RELATE 함수는 지원하지 않는다는 것을 코드로 확인할 수 있고 실제로 MySQL은 위 두 함수를 지원하지 않습니다.
public enum CommonSpatialFunction {
ST_ASTEXT( FunctionKey.apply( "st_astext", "astext" ), 1, StandardBasicTypes.STRING ),
ST_GEOMETRYTYPE( FunctionKey.apply( "st_geometrytype", "geometrytype" ), 1, StandardBasicTypes.STRING ),
ST_DIMENSION( FunctionKey.apply( "st_dimension", "dimension" ), 1, StandardBasicTypes.INTEGER ),
ST_SRID( FunctionKey.apply( "st_srid", "srid" ), 1, StandardBasicTypes.INTEGER ),
ST_ENVELOPE( FunctionKey.apply( "st_envelope", "envelope" ), 1 ),
ST_ASBINARY( FunctionKey.apply( "st_asbinary", "asbinary" ), 1, StandardBasicTypes.BINARY ),
ST_ISEMPTY( FunctionKey.apply( "st_isempty", "isempty" ), 1, StandardBasicTypes.BOOLEAN ),
ST_ISSIMPLE( FunctionKey.apply( "st_issimple", "issimple" ), 1, StandardBasicTypes.BOOLEAN ),
ST_BOUNDARY( FunctionKey.apply( "st_boundary", "boundary" ), 1 ),
ST_OVERLAPS( FunctionKey.apply( "st_overlaps", "overlaps" ), 2, StandardBasicTypes.BOOLEAN ),
ST_INTERSECTS( FunctionKey.apply( "st_intersects", "intersects" ), 2, StandardBasicTypes.BOOLEAN ),
ST_EQUALS( FunctionKey.apply( "st_equals", "equals" ), 2, StandardBasicTypes.BOOLEAN ),
ST_CONTAINS( FunctionKey.apply( "st_contains", "contains" ), 2, StandardBasicTypes.BOOLEAN ),
ST_CROSSES( FunctionKey.apply( "st_crosses", "crosses" ), 2, StandardBasicTypes.BOOLEAN ),
ST_DISJOINT( FunctionKey.apply( "st_disjoint", "disjoint" ), 2, StandardBasicTypes.BOOLEAN ),
ST_TOUCHES( FunctionKey.apply( "st_touches", "touches" ), 2, StandardBasicTypes.BOOLEAN ),
ST_WITHIN( FunctionKey.apply( "st_within", "within" ), 2, StandardBasicTypes.BOOLEAN ),
ST_RELATE( FunctionKey.apply( "st_relate", "relate" ), 2, StandardBasicTypes.STRING ),
ST_DISTANCE( FunctionKey.apply( "st_distance", "distance" ), 2, StandardBasicTypes.DOUBLE ),
ST_BUFFER( FunctionKey.apply( "st_buffer", "buffer" ), 2 ),
ST_CONVEXHULL( FunctionKey.apply( "st_convexhull", "convexhull" ), 1 ),
ST_DIFFERENCE( FunctionKey.apply( "st_difference", "difference" ), 2 ),
ST_INTERSECTION( FunctionKey.apply( "st_intersection", "intersection" ), 2 ),
ST_SYMDIFFERENCE( FunctionKey.apply( "st_symdifference", "symdifference" ), 2 ),
ST_UNION( FunctionKey.apply( "st_union", "geomunion" ), 2 );
즉 위에 명시된 BOUNDARY, RELATE를 제외하곤 MySQL도 많은 함수를 지원한다는 것입니다.
그런데 문서엔 코드와 달리 위 함수들을 JPQL로 지원하지 않는다고 돼 있습니다.
코드만 믿기엔 찝찝해서 직접 위 함수 모두를 직접 테스트해본 결과 JPQL이 SQL로 잘 번역돼 호출되는 것을 확인했습니다.
st_geomunion 같은 경우는 MySQL은 st_union 이라는 함수로 여전히 지원하기도 합니다. (아직 MySQL이 SFS표준을 업데이트 하지 않은 것으로 보이고 JPQL로 st_union을 호출하면 ST_Union 함수로 번역돼 제 기능을 합니다.)
Pull Request
코드 분석과 테스트를 거친 결과 문서 업데이트가 누락된 것이라고 100% 확신했고 저 처럼 다른 개발자들이 혼란을 겪지 않게 하기위해
표를 수정하고 MySQLSpatial56Dialect 에 대한 언급을 지우고 MySQL 링크도 8.4 current로 변경해 Pull Request를 보냈습니다.
HHH-18966 update user guide mysql spatial function support by songhaechan · Pull Request #9507 · hibernate/hibernate-orm
This PR updates the documentation to reflect the availability of several MySQL Spatial Functions based on both the official documentation (MySQL Spatial Relation Functions and MySQL Spatial Operato...
github.com
사실 PR과정에서 많이 삐그덕 거리긴 했습니다...
Hibernate는 JIRA에서 이슈를 관리하고 JIRA의 이슈 번호 HHH-xxxx를 꼭 커밋 메세지 접두어로 써야합니다.
그리고 메인 브랜치에 새로운 변경사항이 발생했다면 merge가 아닌 rebase로 히스토리를 깔끔하게 유지하라고 합니다.
첨에 그냥 merge했다가 꼬여서 고생 좀 했습니다.
hibernate-orm/CONTRIBUTING.md at main · hibernate/hibernate-orm
Hibernate's core Object/Relational Mapping functionality - hibernate/hibernate-orm
github.com
'Spring' 카테고리의 다른 글
RabbitMQ Prefetch와 ConcurrentConsumer의 관계 (0) | 2025.01.05 |
---|---|
동시성 문제 해결하기 (3) | 2024.10.26 |
정렬 알고리즘 [선택정렬, 버블정렬, 삽입정렬] (0) | 2024.09.27 |
Spring OAuth2 Client 흐름 개선시키기 (0) | 2024.09.14 |
Google S2 (3) | 2024.09.11 |