현재는 Postgres 의 BigDecimal 을 사용해서 위도와 경도를 저장하고 있다.
저장하는 정도에서 끝나면, 딱히 개선할 여지가 없었겠지만, 나는 이 데이터를 가지고 거리 계산, 반경 등을 해야하는데, 내가 직접 함수를 작성하기 보다는 이미 작성된 함수를 내가 잘 다루는 편이 좋을 것 같아서 찾아보았다. Postgres 에서 Mysql 보다 좋은 점이 여기 있었다. 물론 이것 때문은 아니지만…ㅎㅎ 장점 중 하나
💁♂️ 유클리드 좌표계(좌표평면)가 아닌 WGS84 좌표계(위도, 경도)를 사용해야 했는데, MySQL의 공간 연산 함수는 WGS84 좌표계에 대한 지원이 미흡했다. 예를 들어, MySQL의 공간 연산 함수 중 하나인 ST_Intersection(두 선분의 교차점 계산)은 WGS84 좌표계를 쓰는 것과 관계없이 좌표평면을 기준으로 계산하기 때문에 예상한 값과 다른 값을 반환하게 된다. 여러 레퍼런스를 찾아보면 공간데이터를 다룰 때 MySQL 보다는 PostgreSQL의 PostGIS가 사용성과 성능 측면에서 월등하다고 한다. 실제로 PostGIS는 공식 문서에서 WGS84 좌표계 역시 지원한다고 나와있다.
하여튼 이런 상황에서 PostGis 를 적용해보자!!!
Mac homebrew 사용해서 PostGis 를 설치해보았다.
- brew install postgresql # postgres 설치
- brew install postgis # postgis 설치
- brew services start postgresql # postgres 시작
- psql postgres # postgres 시작
- CREATE EXTENSION postgis; # console 에서 postgis 설치
spring.jpa.properties.hibernate.dialect=org.hibernate.spatial.dialect.postgis.PostgisPG95Dialect
# 기존 postgres 만 사용했을 때
# spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
# 아래와 같이 작성해도 괜찮다고 하는데, 나는 안되더라구..
# spring.jpa.properties.hibernate.dialect=org.hibernate.spatial.dialect.postgis.PostgisDialect
implementation 'org.hibernate:hibernate-spatial:6.4.4.Final'
DB 툴을 “DBeaver” 사용 중인데, 각자 툴에 맞게 아래 과정을 진행하시면 됩니다.
아래 Local Postgres 에 진입하고, “Available Extensions” 에서 “postgis” 가 있는지 확인합니다.
- 만약 없다면 설치 먼저 하고 오셔야 합니다~
이후 사용 중인 Database 로 “Extension“ 을 가져와야합니다.
- “Extension” 에서 “postgis” 를 가져옵니다.
아래처럼 Postgres PostGis 를 사용하여 Entity 를 작성합니다.
@Column(columnDefinition = "geometry(Point, 4326)")
private Point coordinates;
이 때 Point 라이브러리가 여러 개가 나올텐데, 얘를 쓰는 게 많은 함수를 사용할 수 있어서 좋습니다.
import org.locationtech.jts.geom.*;
@PostMapping
public ResponseEntity<?> createCCTV(@RequestBody CCTVDto cctvDto) {
CCTVEntity createdCCTV = cctvRepository.save(cctvDto.toCCTVEntity());
return ResponseEntity.ok().body(new SuccessResponse(cctvDto.fromEntity(createdCCTV)));
}
import org.locationtech.jts.geom.*;
을 사용합니다.public static CCTVDto fromEntity(CCTVEntity entity) {
CCTVDto dto = new CCTVDto();
dto.setId(entity.getId());
dto.setLocationAddress(entity.getLocationAddress());
dto.setLatitude(entity.getPoint().getY());
dto.setLongitude(entity.getPoint().getX());
return dto;
}
public CCTVEntity toCCTVEntity() {
GeometryFactory geometryFactory = new GeometryFactory();
Point point = geometryFactory.createPoint(new Coordinate(this.longitude, this.latitude));
return CCTVEntity.builder()
.point(point)
.locationAddress(locationAddress)
.build();
}
PostGis 를 데이터베이스에 적용했기 때문에, PostGis 함수를 사용할 수 있습니다.
PostGis 에서는 좌표를 계산할 때, 평면에서의 계산과 구에서의 계산을 둘 다 지원합니다.
제가 사용할 함수는 ST_DWithin
입니다.
파라미터를 보면 알 수 있듯이, 특정 Location 기준으로 Distance 내에 있는 cctv 목록을 반환합니다.
- ST_DWithin - Tests if two geometries are within a given distance.
@Repository
public interface CctvRepository extends JpaRepository<CctvEntity, Long> {
// distance 는 미터(m) 기준입니다.
@Query(value = "SELECT * FROM cctv WHERE ST_DWithin(gps, ST_SetSRID(ST_MakePoint(:longitude, :latitude), 4326), :distance, true)", nativeQuery = true)
List<CctvEntity> findCctvsByDistance(@Param("longitude") double longitude, @Param("latitude") double latitude, @Param("distance") double distance);
}
위 함수는 데이터베이스 Postgres 의 PostGis 함수를 사용하는 것이므로, JPA 가 아닌 SQL 문을 작성해야합니다.
여기서 distance 는 미터(m) 기준입니다. 그리고, 경도와 위도를 받게 되어있는데 Point 객체를 직접 받아도 사용 가능합니다! 저는 Client 에서 경도와 위도를 주기 때문에 그대로 받아서 SQL 문으로 Point 객체를 만든 것입니다.
따라서 Native Query
를 사용하였습니다!!
💁♀️ 네이티브 쿼리란,
SQL을 개발자가 직접 정의해서 사용할 수 있도록 해주는 수동모드
JPQL은 데이터베이스들이 따로 지원하는 것들에 있어 모든 것을 SQL로 자동으로 바꿔주지 않음 (인라인 뷰, UNION, INTERSECT 등등)
쉽게 말해, 어떠한 다양한 이유로 JPQL을 사용할 수 없는 경우나 SQL쿼리를 최적화 해서 데이터 베이스의 성능을 향상시킬 때 JPA는 Native SQL을 통해 SQL을 직접 사용할 수 있는 기능을 제공
이걸 사용하겠다! 는 아니고, 내가 만든 로직이 잘 돌아가는지 확인하기 위해서 제작되었습니다.
위 Repository 함수를 사용하려면 어떤 정보를 받아야되는지, 어떻게 API 요청을 설계했는지 보겠습니다
@GetMapping
public ResponseEntity<?> getAllCctv(
@RequestParam(required = true, value = "longitude") double longitude,
@RequestParam(required = true, value = "latitude") double latitude,
@RequestParam(required = false, defaultValue = "1000", value = "distance") double distance) {
List<CctvDto> CctvList = cctvService.findCctvsNearbyLocationWithinDistance(longitude, latitude, distance);
return ResponseEntity.ok().body(CctvList);
}
x축인
longitude 경도
, y축인latitude 위도
, 범위인distance 거리
를 받습니다.
대신 distance 는 기본값을 사용할 수 있도록 하였고, 범위는 1 km 로 하였습니다.
longitude 와 latitude 는 필수로 받습니다.
경도와 위도가 조금만 달라져도 실제 거리는 많이 차이납니다.
테스트해본 결과, (10.0000, 10.0000) 과 (10.0000, 10.0001) 은 위도 소수점 4번째 자리가 단 ‘1’ 달랐을 뿐이지만, 실제 거리로는 10m 이상 차이가 발생합니다.
여기 코드는 실제로 다른 Controller 나 Service 에서 사용될 예정입니다. 아마도 Kafka 에서 해당 로직을 사용하겠죠..? 저는 그래요
Service 를 작성해봅시다.
이런 식으로 받습니다.
실제로 DB 에 저장이 되고, 빼올 때는 Point 객체를 사용하지만 이걸 Client 에게 줄 때는 Dto 를 사용해서 경도와 위도로 바꾸어 반환합니다. (이렇게 하는 게 좋은지는 모르겠지만…?)
public List<CctvDto> findCctvsNearbyLocationWithinDistance(double longitude, double latitude, double distance) {
List<CctvEntity> cctvEntities = cctvRepository.findCctvsByDistance(longitude, latitude, distance); // distance 기준: m(미터)
return cctvEntities.stream()
.map(CctvDto::fromEntity) // 엔티티를 DTO로 변환
.collect(Collectors.toList());
}
아래처럼 cctv 정보를 미리 DB 에 저장하고, 테스트 해보았습니다.
아주 잘 나오네요!! 하아 힘들었다!
이 질문에 이제까지는 대답하지 못했어요. 사실 뭐가 다른지 잘 몰라요.
둘 다 관계형 DB 인데, 성능이 뭐가 좋고, 읽기에는 이게 좋고 쓰기에는 뭐가 좋다.
이런 말 솔직히 이해 못해요. 내가 성능테스트 해본 거 아니잖아요.
그치만 이제는 여러 데이터베이스 중에 뭘 선택해야되지?? 라는 고민이 들 때
확장이나 지원하는 데이터 타입, 지원하는 함수 등등을 살펴봐라!!
내가 PostGis 를 썼었는데, Mysql 은 이런 거 안되더라!! 라고 말은 할 수 있을 거 같아요 !!