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);
}
}
관련 글