[Spring] RestClient URI Encoding 문제 (feat. 퍼센트 인코딩)

2024. 5. 21. 00:01·Spring/Spring

 

RestClient의 URI 인코딩

DefaultUriBuilderFactory

  • RestClient를 생성할 때 보통 Builder를 사용해 만들게 됩니다.
public interface RestClient {

    ...

    static RestClient.Builder builder() {
        return new DefaultRestClientBuilder();
    }
    
    ....
    
}
  • RestClient.Bulider가 build() 하는 시점에 아래와 같이 DefaultUriBuilderFactory를 기본으로 생성해 가지고 있습니다.
public class DefaultRestClientBuilder {

    ...

    @Override
    public RestClient build() {
        ClientHttpRequestFactory requestFactory = initRequestFactory();
        UriBuilderFactory uriBuilderFactory = initUriBuilderFactory();
        HttpHeaders defaultHeaders = copyDefaultHeaders();
        List<HttpMessageConverter<?>> messageConverters = (this.messageConverters != null ?
                this.messageConverters : initMessageConverters());
        return new DefaultRestClient(requestFactory,
                this.interceptors, this.initializers, uriBuilderFactory,
                defaultHeaders,
                this.statusHandlers,
                messageConverters,
                this.observationRegistry,
                this.observationConvention,
                new DefaultRestClientBuilder(this)
                );
    }
    
    private UriBuilderFactory initUriBuilderFactory() {
	    if (this.uriBuilderFactory != null) {
		    return this.uriBuilderFactory;
	    }
	    DefaultUriBuilderFactory factory = (this.baseUrl != null ?
			    new DefaultUriBuilderFactory(this.baseUrl) : new DefaultUriBuilderFactory());
	    factory.setDefaultUriVariables(this.defaultUriVariables);
	    return factory;
    }
    
    ....
}
  • DefaultUriBuilderFactory의 EncodingMode 기본값을 확인해보면 EncodingMode.TEMPLATE_AND_VALUES입니다.
public class DefaultUriBuilderFactory implements UriBuilderFactory {

    ...
    
    private EncodingMode encodingMode = EncodingMode.TEMPLATE_AND_VALUES;
    
    ...
}

 

DefaultUriBuilderFactory EncodingMode

EncodingMode를 이해하기 전에 먼저 URI 변수와 URI 템플릿에 대해 알고 있어야 합니다. 아래와 같은 형식을 URI 템플릿이라고 부릅니다. {} 중괄호를 사용해 변수를 넣을 수 있는 부분(URI 변수)이며 나머지는 고정값입니다.

https://api.example.com/{version}/users/{userId}/details
  • URI 변수에 해당하는 중괄호 값인 {version}과 {userId}는 어떤 값이든 들어갈 수 있습니다.
https://api.example.com/v1/users/1/details
https://api.example.com/v2/users/33/details

이와 같이 URI를 특정 형식에 맞춰 동적으로 구성 가능한 기술을 URI 템플릿이라고 이해하시면 됩니다.

 

이제 다시 EncodingMode에 대해 알아봅시다. 종류는 아래와 같습니다.

  • TEMPLATE_AND_VALUES: URI 템플릿과 URI 변수 모두 인코딩 합니다.
  • VALUES_ONLY: URI 템플릿을 인코딩하지 않고 URI 변수를 인코딩합니다.
  • URI_COMPONENT: URI 변수를 적용한 후 URI 컴포넌트를 인코딩합니다.
  • NONE: 인코딩을 적용하지 않습니다.

RestClient는 기본적 설정이  TEMPLATE_AND_VALUES 였으므로, URI 템플릿과 URI 변수 모두 인코딩한다고 보시면 될 것 같습니다.

 

 

인코딩 문제?

콜론을 PathVariable로 전달

  • 종종 외부 API의 Path를 보면 중간에 비밀키를 넣어야 하는 경우가 있습니다. 영문과 숫자만 들어있다면 다행이지만, 특수문자가 들어가는 때도 있습니다.
  • 메시지 키 은닉을 위해 PathVariable로 messageKey를 전달해봅시다.
public interface MessageClient {
  @PostExchange("/v1/services/{messageKey}/messages")
  MessageResponse sendMessage(
      @PathVariable String messageKey,
      @RequestBody MessageRequest request);
}
  • messageKey에 할당된 값을 example:sms:XXXXXXXXXX:myApp 라고 해봅시다.
@Service
@Transactional
public class MessageService {

    private final String MESSAGE_KEY;
    private final MessageClient messageClient;
    
    public MessageService(
        @Value("${external-api.message.key}") final String MESSAGE_KEY,
        final MessageClient messageClient) {
        this.MESSAGE_KEY = MESSAGE_KEY;
        this.messageClient = messageClient;
    }
    
    public MessageResponse sendMessage(final Message Request) {
        return messageClient(MESSAGE_KEY, request);
    }
}
  • 하지만 막상 실행해보면 제대로 동작하지 않습니다. 문제가 뭘까요?

 

% 인코딩

  • 문제는 인코딩에 있습니다. 위의 경로로 실제 URI가 인코딩된 모습을 보면 아래와 같습니다.
  • 콜론( : )이 %3A로 인코딩되면서 SecretKey 값이 변형되어 UNAUTHORIZED 에러가 발생했기 때문에 제대로 동작하지 않았던 것입니다.
https://api.example.com/v1/services/example%3Asms%3AXXXXXXXXXX%3AmyApp/messages

 

 

인코딩 문제를 해결하려면?

DefaultUriBuilderFactory의 EncodingMode를 변경

  • 아래가 기존의 RestClient를 생성하는 로직이었습니다.
private RestClient createRestClient(String baseUrl) {
    return RestClient.builder()
        .baseUrl(baseUrl)
        .build();
}
  • DefaultUriBuilderFactory를 선언하고 setEncodingMode 메소드를 사용해 EncodingMode.NONE을 설정한 뒤, uriBuilderFactory 메소드를 통해 RsetClient에 추가해줬습니다. 이제 URI가 인코딩되지 않기 때문에 SecretKey가 변형되지 않고 잘 보내질 겁니다!
private RestClient createRestClient(String baseUrl) {
    DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);
    uriBuilderFactory.setEncodingMode(EncodingMode.NONE);
    
    return RestClient.builder()
        .uriBuilderFactory(uriBuilderFactory)
        .build();
}

 

EncodingMode를 None으로 두면 문제가 없을까요?

  • QueryParam으로 한글이나 특수문자가 넘어오면 문제가 생길 수 있습니다. (하지만 제가 경험한 바로는 외부 API에서 params 값으로 한글이나 특수문자를 넘겨받기를 원하는 스펙이 보편적인 경우는 아닌 것 같습니다.)
  • 이 때는 RestClient로 전달하기 전에 미리 Encoding을 해주고 변수로 넘겨주면 됩니다!
public interface PaymentClient {

    @PostExchange("/payment")
    ExampleResponse confirmPayment(@RequestParam String param);
}
  • UriEncoder를 통해 한글을 인코딩해줘서 RestClient에게 넘기면 됩니다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ExternalPaymentService {

    private final PaymentClient paymentClient;

    public PaymentExample example(final ExampleRequest request) {
      String encodedKorean = UriEncoder.encode(request.koreanStr()); // 한글 및 특수문자 인코딩
      return paymentClient.confirmPayment(encodedKorean);
    }
}

 

 

 

관련 글

  • 스프링의 외부 API 호출, 그리고 RestClient

 

'Spring/Spring' 카테고리의 다른 글
  • [Spring Batch] Tasklet에서 왜 @BeforeStep과 @AfterStep이 동작하지 않을까?
  • [Spring] 스프링 이벤트로 유연한 설계 만들기
  • [Spring] Controller와 어노테이션
  • [Spring] 스프링 MVC 사용을 위한 의존관계 추가
gakko
gakko
좌충우돌 개발기
  • gakko
    MYVELOP 마이벨롭
    gakko
  • 전체
    오늘
    어제
    • 분류 전체보기 (203)
      • Spring (23)
        • Spring (10)
        • Spring Boot (7)
        • Spring Security (1)
        • Hibernate (4)
      • Test (3)
      • 끄적끄적 (6)
      • 활동 (35)
        • 부스트캠프 (23)
        • 동아리 (3)
        • 컨퍼런스 (3)
        • 글또 (5)
        • 오픈소스 컨트리뷰션 (1)
      • 디자인패턴 (0)
      • Git & GitHub (22)
        • Git (13)
        • Github Actions (1)
        • 오류해결 (5)
        • 기타(마크다운 등) (3)
      • 리눅스 (6)
        • 기초 (6)
        • 리눅스 서버 구축하기 (0)
      • Infra (2)
        • Docker (1)
        • Elastic Search (0)
        • Jenkins (1)
        • AWS (1)
      • MySQL (7)
        • 기초 (6)
        • Real MySQL (1)
      • 후기 (3)
        • Udemy 리뷰 (3)
      • CS (26)
        • 웹 기본지식 (0)
        • 자료구조 (13)
        • 운영체제 OS (12)
        • 데이터베이스 (1)
        • 시스템 프로그래밍 (0)
        • 기타 (0)
      • Tools (1)
        • 이클립스 (1)
        • IntelliJ (0)
      • 프로젝트 (1)
        • 모여모여(부스트캠프) (1)
      • JAVA (32)
        • Maven (6)
        • 오류해결 (11)
        • 자바 클래스&메소드 (1)
        • JSP & Servlet (12)
      • Javascript (5)
        • 기초 (3)
        • React (2)
      • Python (28)
        • 파이썬 함수 (9)
        • 알고리즘 문제풀이 (16)
        • 데이터 사이언스 (2)
        • 웹 크롤링 (1)
      • 단순정보전달글 저장소 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • 우진님
  • 공지사항

  • 인기 글

  • 태그

    오류해결
    알고리즘
    java
    자바스크립트
    Spring
    웹개발
    부스트캠프 7기
    스프링부트
    스프링
    Git
    자바
    MySQL
    jsp
    부스트캠프 멤버십
    os
    부스트캠프
    파이썬
    Python
    운영체제
    GitHub
  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.0
gakko
[Spring] RestClient URI Encoding 문제 (feat. 퍼센트 인코딩)
상단으로

티스토리툴바