Project/Performance Improvement

[프로젝트] 1-1. 로직 개선 - PostGIS 도입

_은선_ 2024. 8. 18. 18:43
728x90
SMALL

개요

오늘은 지난번에 소개 드린 "올봄" 프로젝트의 1. 로직 개선을 진행해보려고 합니다.

성능을 개선하는 방법에는 데이터베이스 쿼리 튜닝, 비즈니스 로직 개선 등 다양한 방법이 있습니다.

 

이번에 개선할 프로젝트인 AI 기반 장년층 라이프 케어 서비스 "올봄"에서는 챗봇, 지도, ToDo, 게임, 일자리 총 5가지 기능이 있습니다.

이 중에서도 저는 지도 기능에서 로직 개선을 진행하였습니다.


기존 지도 리스트 반환 API 문제점 

실제 서비스를 사용하던 중, 지도에서 시설 데이터를 렌더링하는 속도가 느리다는 사실을 발견하였습니다.

 

 

 

지도에서 데이터를 렌더링하는 속도가 느린 주된 원인은, 지도의 움직임이 있을 때마다 클라이언트가 화면 내 영역의 말단 위경도 값을 받아 서버에 API 요청을 보내기 때문입니다. 이로 인해 사용자의 위치가 변하지 않았음에도 불구하고 불필요하게 많은 API 요청이 발생하게 됩니다. 지금처럼 사용자가 적은 서비스에서는 큰 문제가 없지만, 사용자가 늘어나면 서버에 과부하가 발생할 가능성이 큽니다.

 

현재 지도 리스트 렌더링하는 로직

1. 사용자가 지도를 이동하거나 줌인/줌아웃합니다.
2. Flutter 앱에서 지도의 움직임을 감지하고, 화면 내 사각형 영역에서 말단의 북서쪽과 남동쪽 위경도를 계산합니다.
3. 클라이언트는 해당 위경도와 시설 타입 정보를 기반으로 /api/map/{type} 엔드포인트에 API 요청을 보냅니다.
    예: /api/map/hospital?swLatitude=..&swLongitude=..&neLatitude=..&neLongitude=..

4. 서버는 요청된 시설 타입과 일치하고, 쿼리 파라미터로 받은 위경도 범위 내에 속하는 시설 리스트를 필터링하여 반환합니다.

즉, 클라이언트에서 지도를 움직일 때마다 해당 API 요청이 발생합니다.

 

기존 지도(Facility) List 반환 쿼리

@Query("SELECT f FROM Facility f WHERE f.latitude BETWEEN :southWestLatitude AND :northEastLatitude AND f.longitude BETWEEN :southWestLongitude AND :northEastLongitude AND f.type = :facilityType")
    List<Facility> findFacilitiesInRectangleAndType(@Param("southWestLatitude") Double southWestLatitude,
                                                    @Param("southWestLongitude") Double southWestLongitude,
                                                    @Param("northEastLatitude") Double northEastLatitude,
                                                    @Param("northEastLongitude") Double northEastLongitude,
                                                    @Param("facilityType") FacilityType facilityType);

 

위의 쿼리는 인자로 받은 북동쪽과 남서쪽 위경도 범위 내에 있는 특정 타입의 시설들을 리스트로 반환하는 쿼리입니다.

 

결론

정리하자면, 현재 로직은 지도의 움직임에 따라 클라이언트에서 불필요하게 많은 API 요청이 발생할 수 있는 로직입니다.

따라서, 캐시 도입에 앞서, 지도 리스트를 반환하는 현재 API 로직을 먼저 개선하기로 했습니다.


지도 리스트 반환 API 개선 - PostGIS 도입 

API 개선 시나리오 

1. 사용자의 현재 위치를 받아옵니다.
2. 클라이언트 측에 저장돼있던 사용자의 이전 위치와 비교해 그 차이가 특정 오차범위 이내이면 API 요청을 생략합니다.
3. 클라이언트는 사용자의 현재 위경도와 시설 타입을 기반으로 /api/map/{type} 엔드포인트에 API 요청을 보냅니다. 이때 limit에 반환할 최대 결과 수를 지정합니다.
    예: /api/map/postgis/hospital?latitude=..&longitude=..&limit=100
4. 서버는 요청된 시설 타입과 일치하고, 사용자의 현재 위치와 가까운 몇 km 반경 내의 시설 리스트를 정렬하여 반환합니다.

 

즉, 클라이언트에서 사용자의 현재 위치를 기반으로 해당 API 요청이 발생합니다.

클라이언트에 저장된 사용자의 이전 위치와 비교해, 차이가 특정 오차범위 내에 있을 경우 서버로 API 요청을 보내지 않습니다.

결론적으로, 이전 방식과 비교해 클라이언트에서 서버로 보내는 API 요청이 불필요하게 많이 발생하는 문제를 해결하였으며, 서버 부하를 감소시켰습니다.

PostGis란?

PostGIS는 PostgreSQL 데이터베이스 관리 시스템을 위한 확장으로, 공간 데이터(geospatial data)를 저장하고 처리할 수 있는 기능을 제공합니다. PostGIS는 표준 SQL에 공간 데이터 유형을 추가하고, 위치 기반 쿼리와 분석을 가능하게 합니다.

1. 공간 데이터 처리 능력

PostGIS는 공간 데이터 처리를 위한 다양한 함수와 연산자를 제공합니다. 이를 통해 공간 데이터에 대한 복잡한 쿼리와 분석을 수행할 수 있습니다.

 

2. 공간 쿼리 지원

사용자의 현재 위치를 중심으로 특정 반경 내의 시설을 찾는 것과 같은 공간 쿼리는 일반적인 SQL로는 구현하기 어렵습니다. PostGIS는 ST_DWithin, ST_Distance, ST_Intersects와 같은 함수들을 제공하여 공간 쿼리를 쉽게 수행할 수 있게 해줍니다.
예를 들어, 사용자의 위치 좌표와 시설들의 좌표 간의 거리를 계산하고, 특정 반경 내에 있는 시설만 필터링하는 작업을 간단하게 수행할 수 있습니다.

 

3. 인덱싱 기능

PostGIS는 공간 데이터에 대한 인덱싱 기능을 제공합니다. 이를 통해 데이터베이스에서 공간 데이터를 빠르고 효율적으로 검색할 수 있습니다. 

 

PostGis 도입

FacilityRepository.java

@Query(value = "SELECT f.* FROM Facility f " +
        "WHERE ST_DWithin( " +
        "  CAST(ST_SetSRID(ST_MakePoint(:longitude, :latitude), 4326) AS geography), " +
        "  CAST(ST_SetSRID(ST_MakePoint(f.longitude, f.latitude), 4326) AS geography), :radius * 1000) " +
        "AND f.type = :facilityType " +
        "ORDER BY ST_Distance( " +
        "  CAST(ST_SetSRID(ST_MakePoint(:longitude, :latitude), 4326) AS geography), " +
        "  CAST(ST_SetSRID(ST_MakePoint(f.longitude, f.latitude), 4326) AS geography)) " +
        "LIMIT :limit",
        nativeQuery = true)
List<Facility> findFacilitiesByPostgis(
        @Param("latitude") Double latitude,
        @Param("longitude") Double longitude,
        @Param("radius") Double radius,
        @Param("limit") int limit,
        @Param("facilityType") String facilityType);

 

[쿼리 개요]

  • 사용자의 현재 위치(위경도), 찾을 반경, 최대 반환 갯수, 시설의 타입을 인자로 받습니다.
  • 요청된 시설 타입과 일치하고, 사용자의 현재 위치와 가까운 몇 km 반경 내의 시설 리스트를 사용자와 가까운순으로 정렬하여 반환합니다.

[함수 설명]

ST_DWithin 함수 : 두 지리적 객체가 특정 거리 내에 있는지 확인하는 함수

ST_Distance 함수 : 두 지리적 객체 간의 거리를 계산하는 함수

ST_SetSRID 및 ST_MakePoint 함수 : 좌표계(SRID)를 설정하고 지리적 객체를 만드는 함수 

 

[쿼리 설명]

ST_DWithin 함수 : 두 지리적 객체가 지정된 거리를 기준으로 가까운지 여부를 확인합니다.

  • 'ST_SetSRID(ST_MakePoint(:longitude, :latitude), 4326)' : 사용자가 제공한 위경도를 SRID 4326으로 설정하여 지리적 객체를 만듭니다. SRID 4326은 WGS84 좌표계를 의미합니다.
  • 'CAST(... AS geography)' : 위에서 만든 지리적 객체를 geography 유형으로 변환합니다. geography 타입은 지구의 곡률을 고려한 측정에 사용됩니다.
  • ':radius * 1000)' : 사용자가 입력한 반경 값을 미터로 변환합니다. (radius는 킬로미터 단위이므로 1000을 곱해 미터로 변경)

즉, 이 조건은 사용자가 제공한 좌표로부터 radius 킬로미터 내에 있는 시설만을 선택합니다.

"WHERE ST_DWithin( " +
    "  CAST(ST_SetSRID(ST_MakePoint(:longitude, :latitude), 4326) AS geography), " +
    "  CAST(ST_SetSRID(ST_MakePoint(f.longitude, f.latitude), 4326) AS geography), :radius * 1000) " +

 

시설의 유형이 사용자가 제공한 facilityType과 일치하는 경우만 선택합니다.

 "AND f.type = :facilityType " +

 

ST_Distance 함수 : 사용자가 제공한 좌표와 각 시설의 좌표 간의 거리를 계산하여, 결과를 가까운순으로 정렬합니다.

  "ORDER BY ST_Distance( " +
        "  CAST(ST_SetSRID(ST_MakePoint(:longitude, :latitude), 4326) AS geography), " +
        "  CAST(ST_SetSRID(ST_MakePoint(f.longitude, f.latitude), 4326) AS geography)) " +

 

반환할 결과의 최대 개수를 limit 변수로 제한합니다.

"LIMIT :limit",

 


FacilityService.java

public List<FacilityListResponse> findFacilitiesWithinPostgis(double latitude, double longitude, double radius, int limit, String type) {
    List<Facility> facilities = facilityRepository
            .findFacilitiesByPostgis(latitude, longitude, radius, limit, type.toUpperCase());

    return facilities.stream()
            .map(FacilityListResponse::from)
            .toList();
}

 

FacilityController.java

@GetMapping("/postgis/{type}")
    public ResponseEntity<List<FacilityListResponse>> getFacilitiesTypeByPostgis(
            @Auth Member member,
            @PathVariable final String type,
            @RequestParam final Double latitude,
            @RequestParam final Double longitude,
            @RequestParam final Double radius,  // km 단위의 반경
            @RequestParam final int limit      // 최대 결과 수
    ) {
        final List<FacilityListResponse> mapResponses;

        mapResponses = facilityService
                .findFacilitiesWithinPostgis(latitude, longitude, radius, limit, type);
        return ResponseEntity.ok(mapResponses);
    }

 

 

 

 

 

 

728x90
LIST