Runnect Node 버전에서는 경로 데이터를 저장할 때, PostgreSQL에서 제공하는 path 데이터 타입을 사용했었다.
값은 아래와 같이 저장된다.
((x1,y1),(x2,y2),(x3,y3))이번에 Runnect를 Spring으로 마이그레이션하면서 경로 데이터를 path 타입이 아닌 PostgreSQL의 PostGIS에서 제공하는 공간 데이터 타입을 사용하기로 했으며 이유는 다음과 같다.
Node.js에서도 path 타입을 사용하기 위해 typecasting을 해주었지만 Spring 버전에서도 이러한 호환성 문제를 안고 갈 만큼 path 타입이 PostGIS 보다 성능적으로 뛰어나진 않다.
더구나 3차 업데이트에서는 공간 정보를 활용한 기능들이 추가될 가능성도 있기 때문에 확장성을 고려하여 PostGIS 를 사용하기로 결정했다.
그런데 현재 우리 DB에는 실제 유저들이 사용하고 있는 course 데이터가 500개 이상 있다..
그렇기 때문에 기존에 path 타입으로 있던 데이터들을 path 타입에서 geometry(LineString, 4326) 타입으로 형변환하는 작업 또한 고려해야 한다..
👉 공간 정보들을 쿼리로 실행할 수 있도록 해주는 확장자
설치하는 것은 아래 글을 참고하자..ㅎㅎ
[Postgresql] PostGIS 설치 - MySQL이 아닌 PostgreSQL을 사용하는 이유
PostgreSQL은 이미 사용하고 있으니 하이버네이트가 공간 데이터를 다루기 위한 라이브러리만 추가한다.
build.gradle
// hibernate-spatial
implementation 'org.hibernate:hibernate-spatial:5.6.11.Final'
course 엔티티
import org.locationtech.jts.geom.LineString;
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Course extends AuditingTimeEntity {
...
@Column(nullable = false, columnDefinition = "geometry(LineString, 4326)")
private LineString path;
...
}
LineString 타입을 사용하도록 하고 columnDefinition도 지정해 주어야 한다!
그렇다면 이제 값을 받아오고 저장하는 것을 해보자!!
우리가 클라이언트로 부터 받아오는 경로 데이터의 형식은 다음과 같다.
[{"lat":"12.33", "long":"23.33"},{"lat":"44.33", "long":"55.33"}]
이제 이 친구를 (x1 y1, x2 y2, x3 y3) 형식으로 바꿔줘야 한다.
우선, 클라이언트 → DB 상황을 보자!
public class CoordinatePathConverter {
public static LineString coorConvertPath(String path) {
List<CoordinateDto> coordinateDtos = new ArrayList<>();
try {
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(path);
for (JsonNode node : jsonNode) {
Double latitude = node.get("lat").asDouble();
Double longitude = node.get("long").asDouble();
coordinateDtos.add(new CoordinateDto(latitude, longitude));
}
return getLineString(coordinateDtos);
} catch (Exception e) {
throw new BadRequestException(ErrorStatus.VALIDATION_COURSE_PATH_EXCEPTION, ErrorStatus.VALIDATION_COURSE_PATH_EXCEPTION.getMessage());
}
}
private static LineString getLineString(List<CoordinateDto> coordinateDtos) {
GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326);
Coordinate[] coordinates = new Coordinate[coordinateDtos.size()];
for (int i = 0; i < coordinateDtos.size(); i++) {
coordinates[i] = new Coordinate(coordinateDtos.get(i).getLatitude(), coordinateDtos.get(i).getLongitude());
}
LineString lineString = geometryFactory.createLineString(coordinates);
return lineString;
}
@Getter
@AllArgsConstructor
private static class CoordinateDto {
private double latitude;
private double longitude;
}
}
coorConvertPath() 는 JSON 형식의 데이터를 받아서 위도, 경도를 추출한 다음 CoordinateDto 리스트를 만든다. 그리고 이 리스트로 LineString 객체를 생성하여 반환한다.getLineString() 는 coorConvertPath() 에서 생성한 CoordinateDto 리스트를 받아 LineString 타입으로 변환된 데이터를 반환한다.GeometryFactory를 통해 생성할 수 있다.geometryFactory.createLineString() 에 여러 점들의 집합인 Coordinate[]를 인자로 넣어주면 끝!!다음으로, DB → 클라이언트 상황을 보자!
클라이언트한테 꺼내줄 때는 List<List<Double>> 형태로 넘겨주기로 약속했었다!
public class CoordinatePathConverter {
public static List<List<Double>> pathConvertCoor(LineString path) {
try {
List<List<Double>> coordinates = new ArrayList<>();
for (int i = 0; i < path.getNumPoints(); i++) {
List<Double> temp = new ArrayList<>();
temp.add(path.getPointN(i).getX());
temp.add(path.getPointN(i).getY());
coordinates.add(temp);
}
return coordinates;
} catch (Exception e) {
throw new BasicException(ErrorStatus.PATH_CONVERT_FAIL, ErrorStatus.PATH_CONVERT_FAIL.getMessage());
}
}
}
이제 서비스단에서 LineString 타입으로 path 데이터를 사용하면 된다!!!
path 타입의 데이터 geometry(LineString, 4326) 타입으로 변환이제 PostgreSQL의 path 타입으로 저장돼 있던 무수히 많은 값들을 geometry(LineString, 4326) 타입으로 변환해 보자!
ALTER TABLE course
ALTER COLUMN path TYPE text;
우선, course 테이블의 타입을 path → text 로 변환!
UPDATE course
SET path = (
SELECT replace('(' || left(transformed_data, length(transformed_data) - 4) || ')', ', ', ',')
FROM (
SELECT regexp_replace(
regexp_replace(
regexp_replace(course.path, ',', ' ', 'g'),
'\)', ', ', 'g'),
'\(|\)', '', 'g') AS transformed_data
FROM course
WHERE course.id = course.id
LIMIT 1
) AS subquery
);
((x1,y1),(x2,y2),(x3,y3)) 형식을 (x1 y1, x2 y2, x3 y3) 형식으로 업데이트한다!
쿼리의 내용은 다음과 같다.
((x1 y1) (x2 y2) (x3 y3))((x1 y1, (x2 y2, (x3 y3, , x1 y1, x2 y2, x3 y3, , (x1 y1, x2 y2, x3 y3) (x1 y1, x2 y2, x3 y3)UPDATE course
SET path = ST_GeomFromText('LINESTRING(' || substring(path from 2 for length(path) - 2) || ')', 4326);
ST_GeomFromText 함수를 이용하여 좌표를 geometry(LineString, 4326) 타입의 좌표로 변환!
ALTER TABLE course
ALTER COLUMN path TYPE geometry(LineString, 4326);
path 컬럼의 데이터 타입을 geometry(LineString, 4326) 으로 변경!
ALTER TABLE course
ALTER COLUMN path TYPE text;
UPDATE course
SET path = (
SELECT replace('(' || left(transformed_data, length(transformed_data) - 4) || ')', ', ', ',')
FROM (
SELECT regexp_replace(
regexp_replace(
regexp_replace(course.path, ',', ' ', 'g'),
'\)', ', ', 'g'),
'\(|\)', '', 'g') AS transformed_data
FROM course
WHERE course.id = course.id -- Add a condition to make sure it selects a single row
LIMIT 1 -- Limit the subquery to return only one row
) AS subquery
);
UPDATE course
SET path = ST_GeomFromText('LINESTRING(' || substring(path from 2 for length(path) - 2) || ')', 4326);
ALTER TABLE course
ALTER COLUMN path TYPE geometry(LineString, 4326);