Published on

IP 기반 위치 조회 구현 개인적인 정리

Authors
  • avatar
    Name
    김민석
    Twitter

Introduction

개요

IP 기반으로 사용자 위치를 조회하는 기능을 구현하는 과정에서 여러 문제가 발생했다.

특히, X-Forwarded-For 헤더 문제, 공인 IP 조회 방식, Argument Resolver 적용 과정에서의 예외 발생 등의 이슈를 경험했다.

이러한 문제들을 해결한 과정을 상세히 다루고자 한다.

상황

클라이언트의 IP를 기반으로 위치 정보를 조회하는 기능이 필요했다.

외부 API를 활용하여 IP 정보를 가져오고, 해당 IP의 위치 정보를 조회하는 방식을 고려했다.

주요 요구사항:

  • 클라이언트의 IP를 추출하여 위치 정보를 가져와야 한다.

  • API를 활용하여 공인 IP를 조회하고, 해당 IP의 위치 정보를 반환해야 한다.

  • 로컬 환경에서도 운영 환경에서 동일한 방식으로 동작해야 한다.

  • 컨트롤러에서 직접 서비스를 호출하지 않고, Argument Resolver를 활용하여 위치 정보를 자동으로 주입해야 한다.

문제 1) X-Forwarded-For 헤더가 null로 출력됨

발생한 문제

image.png
  • API를 통해 클라이언트의 IP를 조회하려 했으나, X-Forwarded-For 값이 항상 null로 출력되는 문제가 발생했다.
  • 이미지와 같이, curl 요청을 보냈을 때 "원격 서버에 연결할 수 없습니다."라는 오류가 발생했다.

원인 분석

  • X-Forwarded-For 헤더는 프록시 서버(Nginx, AWS Load Balancer 등)를 거칠 때만 추가된다는 것이였다.. (아는 만큼 보인다고, 난 당연히 될거라고 생각 했다..)
  • 로컬 환경에서 직접 실행하는 경우, 프록시를 거치지 않기 때문에 기본적으로 포함되지 않았다.
    • request.getRemoteAddr()를 사용하면 내부 네트워크 IP(예: 127.0.0.1)가 반환됨.
  • X-Forwarded-For 헤더를 사용하려 했으나, 로컬 환경에서는 항상 null이었다..
  • request.getRemoteAddr()를 사용했지만, 운영 환경에서는 프록시를 거쳐야 하므로 일관성이 없었다고 판단.

해결 방법 1) 공인 IP 조회 기능 적용

  • 로컬 환경에서도 실제 공인 IP를 조회할 수 있도록, 외부 API(https://checkip.amazonaws.com)를 활용하는 방식을 적용했다.

적용한 방법

  • PublicIpProvider 클래스를 생성하여 공인 IP를 가져오는 기능을 별도로 분리 했다.
  • RestTemplate을 활용하여 외부 API를 호출하고 공인 IP를 반환 했다.
  • GeoLocationService에서 로컬 환경에서는 공인 IP를 가져오도록 처리하도록 구현

적용 코드

@Component
@RequiredArgsConstructor
public class PublicIpProvider {
    private static final String PUBLIC_IP_API = "https://checkip.amazonaws.com";
    private final RestTemplate restTemplate;

    public String getPublicIp() {
        try {
            return restTemplate.getForObject(PUBLIC_IP_API, String.class).trim();
        } catch (Exception e) {
            throw new RuntimeException("공인 IP 조회 실패", e);
        }
    }
}

적용 후 결과

  • 로컬 환경에서도 정상적으로 공인 IP를 가져올 수 있게 되었다.
  • 운영 환경에서는 기존 X-Forwarded-For 헤더를 활용하는 방식 유지.

문제 2) API 호출 중 NumberFormatException 발생

발생한 문제

image.png
  • 공인 IP 조회 기능을 적용한 후, 외부 API에서 반환된 데이터를 처리하는 과정에서 NumberFormatException이 발생했다.
  • 이미지와 같이, "empty String"을 Double.parseDouble()로 변환하려다 예외가 발생했다.

원인 분석

  • API 응답에서 loc 필드(위도, 경도 정보)가 비어 있는 경우가 존재함.
  • "loc": "37.386,-122.084" 형식의 문자열을 split(",")하여 파싱해야 하는데, 값이 비어 있으면 NumberFormatException이 발생 했다.
  • 단순히 Double.parseDouble()을 사용했으나, 값이 비어 있을 경우를 처리하지 않아 예외가 발생
  • try-catch 블록 없이 split(",")을 수행했기 때문에, 비어 있는 값이 그대로 전달되면서 오류가 발생

해결 방법 2) 데이터 변환 로직 개선

API 응답 데이터에서 위도와 경도 정보를 추출하는 extractLatLong() 메서드를 개선했다.

private double[] extractLatLong(JsonNode root) {
    double latitude = 0.0;
    double longitude = 0.0;

    String loc = root.path("loc").asText(); // "37.386,-122.084"
    if (loc.contains(",")) {
        String[] latLong = loc.split(",");
        try {
            latitude = Double.parseDouble(latLong[0]);
            longitude = Double.parseDouble(latLong[1]);
        } catch (NumberFormatException e) {
        }
    }
    return new double[]{latitude, longitude};
}

적용한 방법

  • loc 필드가 비어 있거나 "bogon": true인 경우 기본값(0.0, 0.0)을 반환한다.
  • split(",")을 수행하기 전에 값이 유효한지 확인한다.
  • 예외 발생 시 안전하게 기본값을 반환하도록 try-catch 블록 추가

Argument Resolver 적용하여 자동 주입

IP는 컨트롤러 앞단에서 처리하는 게 좋다?
  • IP 추출은 컨트롤러 앞단에서 처리하는 게 좋다.
  • 이유는 IP는 모든 요청에서 동일한 방식으로 처리되므로, 요청이 들어오는 순간 한 번만 추출하면 되기 때문이다.
  • 즉, 모든 컨트롤러에서 중복으로 IP를 추출하는 것보다, 한 곳에서 공통으로 처리하는 것이 좋다고 판단했다.

결론 & 얻은 것

문제 해결.

  • X-Forwarded-Fornull이 되는 문제를 공인 IP 조회 기능으로 해결 했다.
  • Argument Resolver를 적용하여 한 곳에서 공통으로 처리 했다.
  • 로컬과 운영 환경에서 동일한 방식으로 동작하도록 개선했다.

얻은 것.

  • IP 기반 위치 조회 시스템을 구축할 때 고려해야 할 사항을 학습했다.
  • 로컬과 운영 환경의 차이를 고려한 해결 방법 적용 경험했다.
  • Argument Resolver를 활용한 자동 주입 방식의 이점 경험했다.