1. 문제상황
공공데이터 포탈 API를 사용하는 과정에서 약간의 어려움을 겪었다. 포스트맨을 이용하여 호출시 정상 응답을 받았지만, 자바 애플리케이션에서 호출하니 'SERVICE_KEY_IS_NOT_REGISTERED_ERROR' 라는 응답을 받았다. 보통 API 키를 헤더에 넣어서 전송하지만 공공데이터 포탈 API를 사용할때는 쿼리 파라미터로 전송해야한다. 이 때문에 발생한 인코딩 문제의 원인을 파악하는 과정과 해결하는 방법을 이번 포스트에서 다뤄보고자 한다.
2. 문제 원인 분석
내가 겪은 문제는 인코딩된 API 키로 요청했음에도 'SERVICE_KEY_IS_NOT_REGISTERED_ERROR' 응답을 받는 것이었다. 공공데이터 포탈에 올라와있는 Q&A에서도 API Key 값을 쿼리 파라미터로 전송시 UTF-8로 인코딩을 해야한다고 설명하고 있다.
그런데 나는 공공 데이터 포털에서 제공하는 '인코딩된 Key'값을 사용하고 있는데 왜 문제가 발생했을까?
결론부터 말하면, SpringBoot 에서 제공하는 RestTemplate의 API 호출 메소드 내부에서 파라미터로 넘긴 URL을 한번 더 인코딩하기 때문에 문제가 발생했던 것이다. 즉 인코딩을 두번하기 때문에 올바른 API Key 값이 전달되지 않은 것이다.
이제부터 문제의 원인을 파악하는 과정을 살펴보자.
2.1 Endpoint에 요청을 전송하는 코드
- url은 서비스 키 값을 포함한 API endpoint를 의미한다.
- 호출하는 과정을 살펴보기 위해 RestTemplate은 직접 생성해서 사용하고있다.
- 문제는 exchange() 메소드에 있을 것이므로, 어떻게 구현되어 있는지 살펴보자.
2.2 RestTemplate 구현 살펴보기
- 상당히 복잡하다. 우리가 관심있는 것은 url 이므로 return 주변에 있는 this.exectue 메소드를 살펴보자
- 이 메소드에서 내가 전달한 url 파라미터가 URI 클래스로 변환된다
- 변환되는 과정을 구체적으로 살펴보기 위해 expand 메소드를 살펴보자
- UriTemplateHandler 인터페이스를 구현한 클래스가 아래와 같이 두개 존재한다.
RootUriTemplateHandler, DefaultUriBuilderFactory - 아래와 같은 코드로 확인해본결과 디폴트로 DefaultUriBuilderFactory를 사용함을 알게 되었다.
2.3 DefaultUriBuilderFactory 의 expand() 메소드
- expand 메소드는 URI 객체 생성을 위임하는 메소드이다.
- uriString 메소드에서 사용된 생성자를 살펴보자
- initUriComponentBuilder 메소드의 흐름을 잘 따라가보면 빨간색으로 표시한 것처럼 인코딩과 관련된 코드가 존재함을 알 수 있다.
- DefaultUriBuilderFactory의 encodingMode의 필드 값에 따라 인코딩을 여부를 결정함을 알 수 있다.
- 아래 첨부한 것과 같이 기본값은 TEMPLATE_AND_VALUES 임을 알 수 있다.
즉 RestTemplate의 exchange() 메소드를 호출하면서 URL 인코딩을 한번 더 하는 것이 문제의 원인이었다!
3. 해결
3.1 EncodingMode를 none으로 수정
- 2.1에서 제시한 코드에서 RestTemplate의 uriTemplateHandler를 가져와 인코딩 모드를 none으로 바꿔주었다. 그 결과, API 응답이 아래와같이 정상적으로 반환됨을 알 수 있다.
그런데, 이러한 방식은 스프링부트의 철학과는 맞지 않다. uriTemplateHandler를 빈으로 등록해서 RestTemplate에 주입하려고 했으나, RestTemplate은 uriTemplateHandler를 생성자 혹은 Setter로 변경하는 방식을 지원하지 않는다. 오히려, 생성자 내부에서 initUriTemplateHandler 라는 메소드를 이용하여 객체를 직접 생성하여 사용한다. 그렇다면, 아래와 같이 RestTemplate을 빈으로 등록하는게 최선일 것 같다.
3.2 HttpURLConnection 클래스 활용
위의 문제 원인을 파악하며 공공데이터 포탈 API는 java.net 에서 제공하는 URL, HttpURLConnection 와 같은 클래스를 사용하는게 나을 수도 있다고 느꼈다. 2년 전에 수강한 네트워크 프로그래밍에서 학습한 내용을 찾아보며 아래와 같이 간단하게 문제를 해결할 수 있었다. 이 코드에서 주목할 점은 주석에 명시한 것 처럼 connection.getResponseCode()를 호출할때 실제로 서버에 요청을 전송한다는 점이다.
- HTTP 커넥션 풀을 사용하지 않기 때문에 운영환경에는 적절하지 않을 수 있다.
- java.net에서 제공하는 클래스들의 구체적인 사용법은 다른 포스트에서 정리해보겠다!
4. 후기
약 2년간 스프링 프레임 워크를 사용하면서 점차 자바의 순수 라이브러리 사용법에 무뎌졌던 것 같다.
이번 계기를 통해 스프링 프레임워크도 결국 자바 라이브러리를 추상화한 것임을 상기하게 되었다.
내가 예상한대로 코드가 동작하지 않을때, 오픈소스를 파헤쳐보며 문제 원인을 파악하고 해결하는 습관의 첫걸음을 내디딘 것 같다!
'트러블슈팅' 카테고리의 다른 글
도커 악성 코드가 차단됨: com.docker.vmnetd에 악성코드가 포함되어 있어서 열리지 않았습니다. (1) | 2025.02.03 |
---|