스프링 반경 검색 기능 - DB 변경에 대한 고민(MySQL vs PostgreSQL)

Jang990·2023년 8월 12일
2
post-thumbnail

현재 상황

지금 진행하고 있는 프로젝트에서는 MySQL을 사용하고 있습니다.

공간 검색 기능에 대해 알아보던 중 다음 글을 발견했습니다.
https://steemit.com/kr-dev/@tmkor/db-2-mysql-vs-postgis-postgresql

해당 글의 결과만 보자면 다음과 같습니다.

  1. PostGIS의 gist index의 경우 MySQL의 spatial index에 비해 10x 이상의 성능 향상을 가져다 줍니다.
  2. 다만, PostGIS의 경우 지원 문서 및 사용자 수가 적으므로, 상황에 맞추어 선택을 하는 것이 좋습니다.

지금까지 프로젝트를 진행하면서 팀원 모두가 MySQL이 익숙하기 때문에 사용했습니다.
하지만 현재 진행하고 있는 프로젝트는 지도 관련 공간 검색의 비중이 매우 높습니다.
그렇기 때문에 팀원과 상의해본 결과 "PostgreSQL + PostGIS를 사용해서 성능이 10배 이상 빨라진다면 사용하지 않을 이유가 없다는 것"이 결론이였습니다.

테스트 해보기

하지만 글만 보고는 믿을 수가 없으니 직접 테스트를 해보겠습니다.
PostgreSQL와 PostGIS를 쓰면 정말 빠를지 스프링에서 처리 시간을 테스트해보겠습니다.

관련 코드는 다음 링크로 확인할 수 있습니다.
https://github.com/Jang990/Radius_Search

MySQL

MySQL에서 반경 검색을 하는 방법은 2가지가 있습니다.

  1. ST_Distance_Sphere로 거리를 일일이 비교하기
  2. ST_Contains 또는 ST_Within로 필터링을 하고서 ST_Distance_Sphere로 거리를 비교하기

1번 방법

단순하게 현재 위치를 기준으로 거리를 계산해서 3KM 이내의 데이터만을 뽑아내는 방식입니다.

@Repository
public interface ClientRepository extends JpaRepository<Client, Long> {
    ...
	
    // 1번 방법
    @Query(value = "select c from Client c " +
            "where ST_Distance_Sphere(c.location.location, :point) <= 3000")
    List<Client> findMySQLST_Distance(@Param("point") Point point);
    
}
	// 테스트 코드
	@Test
    void test1() {
        ClientLocation location = new ClientLocation(35.001d, 125.001d);
        List<Client> result = clientRepository.findMySQLST_Distance(location.getLocation());
        // 약 475ms
    }

1만 건의 데이터 기준 MySQL의 ST_Distance_Sphere은 약 475ms의 시간이 소요되었습니다.

2번 방법

사진과 같이 Polygon 사각형을 만들어 ST_Contains P2, P4, P6를 가져옵니다.
ST_Distance를 활용해서 P6를 제외합니다.

@Repository
public interface ClientRepository extends JpaRepository<Client, Long> {
    ...
	
    // 2번 방법
    @Query(value = "select c from Client c where " +
            "ST_Contains(:polygon, c.location.location) " +
            "AND ST_Distance_Sphere(c.location.location, :point) <= :distanceMeter")
    List<Client> findMySQLST_Contains(Polygon polygon, @Param("point") Point point, int distanceMeter);
    
}
    @Test
    void test2() {
        ClientLocation location = new ClientLocation(35.001d, 125.001d);
        Polygon polygon = createSquareAroundPoint(35.001d, 125.001d, 3);
        List<Client> result = clientRepository.findMySQLST_Contains(polygon, location.getLocation(), 3000);
        // 약 315ms
    }

    public Polygon createSquareAroundPoint(double latitude, double longitude, double radiusInKm) {
    	// GPT 도움... - 부정확할 수 있음
        GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326);

        double halfSideLength = radiusInKm / (111.32 * Math.cos(Math.toRadians(latitude)));
        Coordinate[] coordinates = new Coordinate[5];
        coordinates[0] = new Coordinate(longitude - halfSideLength, latitude - halfSideLength);
        coordinates[1] = new Coordinate(longitude + halfSideLength, latitude - halfSideLength);
        coordinates[2] = new Coordinate(longitude + halfSideLength, latitude + halfSideLength);
        coordinates[3] = new Coordinate(longitude - halfSideLength, latitude + halfSideLength);
        coordinates[4] = coordinates[0];  // Close the ring

        Polygon square = geometryFactory.createPolygon(coordinates);

        return square;
    }

1만 건의 데이터 기준 MySQL의 ST_Contains는 약 315ms의 시간이 소요되었습니다.

PostgreSQL + PostGIS

PostgreSQL과 PostGIS 설치는 다음 글을 참고해주세요.
PostgreSQL + PostGIS설치하기(Windows 환경)

PostgreSQL + PostGIS에서도 2가지 방법이 있습니다.
1. PostGIS에서 제공하는 ST_DistanceSphere로 거리를 일일이 비교하기
2. PostGIS에서 제공하는 ST_Dwithin로 반경내의 값을 가져오기

1번 방법

MySQL과 마찬가지로 단순하게 현재 위치를 기준으로 거리를 계산해서 3KM 이내의 데이터만을 뽑아내는 방식입니다.

@Repository
public interface ClientRepository extends JpaRepository<Client, Long> {
    ...

	// 1번 방법
    @Query(value = "select c from Client c " +
            "where ST_DistanceSphere(c.location.location, :point) <= 3000")
    List<Client> findPostgreSQLST_Distance(@Param("point") Point point);
    
}
	@Test
    void postdb_distance() {
        double nowX = 125.001;
        double nowY = 35.001;
        ClientLocation location = new ClientLocation(nowY, nowX);

        List<Client> result = clientRepository.findPostgreSQLST_Distance(location.getLocation());
        // 1만 건 기준 약 400ms
    }

1만 건의 데이터 기준 PostGIS의 ST_DistanceSphere은 약 400ms의 시간이 소요되었습니다.

2번 방법

PostGIS는 특정 거리 내의 데이터를 뽑아내주는 함수를 제공하고 있습니다.
ST_Dwithin이 바로 그 함수입니다.
해당 함수는 주어진 거리 내에 도형이 존재하면 true를 반환합니다.

@Repository
public interface ClientRepository extends JpaRepository<Client, Long> {
    ...
    
    // 2번 방법
    @Query(value = "select c from Client c " +
            "where ST_Dwithin(c.location.location, :point, 3000, false) = true")
    List<Client> findTestST_Dwithin(@Param("point") Point point);

}
	@Test
    void postdb() {
        double nowX = 125.001;
        double nowY = 35.001;
        ClientLocation location = new ClientLocation(nowY, nowX);

        List<Client> result = clientRepository.findTestST_Dwithin(location.getLocation());
        // 1만 건 기준 약 20ms
    }

PostGIS의 ST_Dwithin은 1만 건 기준 20ms의 시간이 소요되었습니다.

결론

PostgreSQL ST_Dwithin : 약 20ms

MySQL ST_Contains+ST_Distance_Sphere 함께 사용 : 약 315ms

PostgreSQL ST_DistanceSphere : 약 400ms

MySQL ST_Distance_Sphere 함께 사용 : 약 475ms

ST_Dwithin과 ST_Contains 성능 비율
20ms : 315ms = 1 : 16

PostGIS를 이용한 공간 검색의 성능 차이가 너무 압도적이여서
PostgreSQL로 DB를 변경하기로 결정했습니다.

참고

도서 - Real MySQL 8.0

profile
공부한 내용을 적지 말고 이해한 내용을 설명하자

0개의 댓글