본문 바로가기
IT/Spring boot

[Spring boot] 위도 경도를 이용하는 거리 계산 알고리즘

by kyu-nahc 2024. 9. 12.
반응형

 

프로젝트 개요

요즘 모바일 웹이나 앱을 이용하다 보면 스마트폰의 현재 위치를 기반으로

특정 거리 안에 있는 정보를 사용자에게 제공하는 서비스들을 쉽게 접할 수 있다.

이때 해당 정보를 보여주기 위해서는 현재 위치와 특정 정보가 존재하는 위치가 필요하다.

두 지점 간의 거리를 계산해야 가까운 순서대로 정렬을 하거나 거리를 가지고 필터링을 할 수 있다.

 

프로젝트를 진행하기 위해서 공공데이터 포털이나, 다른 문화데이터 광장 같은 곳에서

데이터를 많이 받아오는데, 이때 장소에 대한 데이터도 많이 존재한다.

장소에 대한 데이터를 보면 해당 장소의 위도, 경도를 제공해 주는 경우가 많다.

이를 이용하여 사용자의 위치로부터 데이터베이스에 존재하는

데이터의 거리를 계산하여 거리 순으로 데이터를 정렬하여 반환해 줄 수도 있다.

위도, 경도의 정보만 존재한다면 특정 거리 안에 존재하는 장소 데이터를 쉽게 제공해 줄 수 있다.

아래 Java 코드들은 Spring 프로젝트에서 진행하였던 코드로,

위도, 경도를 이용한 거리 계산 및 정렬 알고리즘을 포함하고 있다.

먼저 개발환경부터 보면 다음과 같다.

 

개발 환경

  • Spring boot 3.3.3
  • Java 21
  • Maven
  • Spring Security / Jpa / Redis
  • Maria Database
@Getter
@Setter
@NoArgsConstructor
@Entity(name = "place")
public class Place {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String placeName;
    private String category;
    private String regionCode;
    private double latitude;
    private double longitude;
    private String address;
    ... code
}

 

해당 클래스는 Persistent Context 내에서 관리되는 Entity 객체이다.

Maria DB에 있는 place table과 바로 Mapping 되며, 장소에 대한 데이터를 저장하는 용도이다.

거리를 직접 계산하기 위해 위도, 경도에 대한 정보도 존재하는 것을 볼 수 있다.

@AllArgsConstructor
@Getter
public class CoordinateRequest {
    private double latitude;
    private double longitude;
}

 

해당 클래스의 멤버변수로는 위도, 경도가 있으며,

사용자의 현재 위치에 대한 위도, 경도 값을 데이터로 받기 위해 존재한다.

Post 요청에 대해 @RequestBody를 사용하여 위도, 경도에 대한

Json 포맷의 값을 CoordinateRequest Class로 Mapping 하여 받는다.

@AllArgsConstructor
@Getter
public class LocationRequest {
    private double startLatitude;
    private double startLongitude;
    private double endLatitude;
    private double endLongitude;
}

 

해당 클래스의 멤버변수로는 시작 위치위도, 경도/ 도착 위치 위도, 경도가 존재한다.

거리를 계산하기 위해 두 장소의 위도, 경도 정보가 필요한데,

이에 대한 데이터를 전달하기 위한 DTO 객체이다.

@NoArgsConstructor
@AllArgsConstructor
@Getter
@Builder
public class DistancePlaceResponse {
    private Place place;
    private double distance;
    private LocationRequest locationRequest;
}

 

해당 클래스의 멤버변수로는 Entity로 관리되는 Place 객체와

사용자의 현재 위치에서 해당 Place까지의 거리를 저장하는 distance 변수가 존재한다.

해당 distance 변수를 통해 Comparator를 이용하여,

DistancePlaceResponse 객체를 거리순으로 정렬할 수 있다.

LocationRequest에는 사용자의 현재 위치와 해당 Place 객체의 위도, 경도 값이 들어가 있다.

@PostMapping("/around/{category}")
public ResponseEntity<List<AroundPlaceResponse>> getAroundPlace(
        @RequestBody CoordinateRequest coordinatesRequest,
        @PathVariable String category
) {
    return ResponseEntity.ok(placeService.getAroundPlaceLotProcess(coordinatesRequest,category));
}​

 

해당 로직은 컨트롤러 내부에 있는 Post 요청을 받는 메소드이다.

@RequestBody를 통해 사용자의 현재 위치에 대한 위도, 경도 정보를 데이터로 받는다.

@PathVariable를 통해 Url에 들어오는 값으로 category를 Mapping 한다.

getAroundPlaceLotProcess() 메소드로

사용자의 현재 위, 경도 좌표와, 카테고리 값을 매개변수로 전달하여,

실제 거리 계산 및 정렬 프로세스를 실행한다.

@Transactional(readOnly = true)
public List<AroundPlaceResponse> getAroundPlaceLotProcess(
        CoordinateRequest coordinateRequest, String category) {
    List<DistancePlaceResponse> distancePlaceList = getAllCalculateAndSortByDistProcess(
            coordinateRequest, category
    );
    return getRouteInfoProcess(distancePlaceList.subList(0, Math.min(5, distancePlaceList.size())));
}

@Transactional(readOnly = true)
public List<DistancePlaceResponse> getAllCalculateAndSortByDistProcess
(CoordinateRequest coordinateRequest,String category){

    List<Place> allCategoryPlaceList;
    if (category.equals("all")){
        allCategoryPlaceList = placeRepository.findAll();
    }else{
        allCategoryPlaceList = getPlaceByCategoryProcess(category);
    }
    List<DistancePlaceResponse> distancePlaceList = new ArrayList<>();
    for(Place place : allCategoryPlaceList){
        LocationRequest locationRequest = new LocationRequest(
                coordinateRequest.getLatitude(),
                coordinateRequest.getLongitude(),
                place.getLatitude(),
                place.getLongitude()
        );
        distancePlaceList.add(
                new DistancePlaceResponse(
                        place,getDistanceProcess(locationRequest),locationRequest
                )
        );
    }
    distancePlaceList.sort(Comparator.comparingDouble(DistancePlaceResponse::getDistance));
    return distancePlaceList;
}

 

해당 로직들은 서비스 계층에 존재하는 로직들로, 

getAroundPlaceLotProcess()  메소드에서  컨트롤러에서 전달한 현재 위치 좌표와,

카테고리를 매개변수로 전달받는다. 그 후 실질적인 거리 계산을 하는 프로세스인

getAllCalculateAndSortByDistProcess()를 호출한다.

Repository에 존재하는 DB Connection 및 메소드를 이용하여,

모든 장소 데이터 혹은 카테고리 별 장소 데이터를 받은 후,

for문을 돌면서 장소 데이터에 대해 거리를 측정한다.

이때 LocationRequest 객체를 생성하여, 매개변수로 전달받은

사용자의 현재 위도, 경도 목표 장소의 위도, 경도의 값을 할당한다.

이제 거리 계산을 진행하는 getDistanceProcess()는 다음과 같다.

// 두 지점의 거리 계산
// startLongitude - 시작 위치 경도
// startLatitude - 시작 위치 위도
// endLongitude - 도착 위치 경도
// endLatitude - 도착 위치 위도
private double getDistanceProcess(LocationRequest locationRequest) {
    double theta = locationRequest.getEndLongitude() - locationRequest.getStartLongitude();
    double dist = Math.sin(deg2rad(locationRequest.getStartLatitude())) *
            Math.sin(deg2rad(locationRequest.getEndLatitude())) +
            Math.cos(deg2rad(locationRequest.getStartLatitude())) *
                    Math.cos(deg2rad(locationRequest.getEndLatitude())) *
                    Math.cos(deg2rad(theta));
    dist = Math.acos(dist);
    dist = rad2deg(dist);
    dist = dist * 60 * 1.1515 * 1609.344;
    return dist / 1000;
}
// 10진수를 radian(라디안)으로 변환
private double deg2rad(double deg) {
    return (deg * Math.PI / 180.0);
}
// radian(라디안)을 10진수로 변환
private double rad2deg(double rad) {
    return (rad * 180 / Math.PI);
}

 

위의 getDistanceProcess() 메소드에서 거리계산을 실시한다.

먼저 LocationRequest의 멤버변수를 다시 살펴보면 다음과 같다.

  • startLongitude - 시작 위치 경도
  • startLatitude - 시작 위치 위도
  • endLongitude - 도착 위치 경도
  • endLatitude - 도착 위치 위도

4개의 위도, 경도 좌표를 이용해서 위의 수식을 통해 두 지점의 거리를 구할 수 있다.

이때 return 되는 값의 단위는 meter이다.

return 되는 값은 DistancePlaceResponse DTO Class의 distance의 값으로 할당되고,

distance 값을 통해 List<DistancePlaceResponse>들을 정렬할 수 있다.

 distancePlaceList.sort(Comparator.comparingDouble(DistancePlaceResponse::getDistance));

 

위의 구문이 List의 객체들을 distance 값을 기준으로 오름차순 정렬하는 코드이다.

이를 통해 최종 Client 측으로 거리순으로 정렬된 데이터를 반환가능하다.

해당 프로젝트에서는 거리순으로 정렬된 데이터에 교통정보를 포함하기 위해

다른 서비스 로직을 한 번 더 거친 결과를 반환하도록 구현하였다.

 

이처럼 요즘 서비스들을 보면 사용자 위치를 기준으로 주변 장소의 정보를 알려주는 서비스가 많은데,

장소 데이터의 위도, 경도 정보만 존재한다면 주변 장소를 제공하는 로직을 구현할 수 있다.

kakao 혹은 TMap을 통해 주변 장소 검색 Api를 이용할 수도 있지만,

Api를 호출하는 과정에서 너무나 많은 시간이 소비될 수도 있고,

해당 외부 서버에 문제가 생긴다면 개인 서버에서도 해당 서비스를 제공하지 못하게 된.

따라서 외부 Api를 이용하는 것보다 직접 로직을 작성하여 제공하는 것이

시간이나 예외 처리 부분에서도 유용하다고 할 수 있다.

 

반응형

loading