가끔 RestTemplate 에서 사용할 복잡한
URL
작성을 위해서 UriComponentBuilder
를 사용하는 경우가 종종 있다.
그런데 주의할 게 있다!
UriComponentBuilder 에서 url encoding 이 1번,
RestTemplate 메소드 호출에서 url encoding 이 또 1번 일어나서,
결과적으로 query parameter 로 전송하는 값들이 우리가 기대했던 대로 가지 않을 수 있다.
코드를 짜고 눈으로 확인해보자.
코드 작성 환경은
Spring Boot (v.2.7.1)
+gradle
이며,
gradle의dependencies
설정은 아래와 같다.dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' implementation 'org.apache.httpcomponents:httpclient' }
@SpringBootApplication
@RestController
public class SpringRestTemplateApplication {
public static void main(String[] args) {
SpringApplication.run(SpringRestTemplateApplication.class, args);
}
@GetMapping(value = "/good/morning/{requestId}")
public String getMethod(@RequestParam MultiValueMap<String, ?> good,
@PathVariable String requestId) {
System.out.println("================" + requestId + "==================");
good.forEach((s, objects) -> {
System.out.println(s + ": " + objects);
});
System.out.println();
return "good";
}
}
new RestTemplate.getForEntity(~)
메소드를 통해서 테스트를 할 예정이다.
여기서 집중해서 봐야할 것은
UriComponentBuilders
의 encode
메소드 사용여부new RestTemplate.getForEntity(~)
메소드의 첫번째 인자값 형태UriComponents.toUri
사용UriComponents.toUriString
사용 모든 경우의 수를 생각하면 4가지 조합이 나온다.
UriComponents.toUri / toUriString | UriComponentsBuilder.encode() 사용 여부 |
---|---|
toUri | O |
toUri | X |
toUriString | O |
toUriString | X |
위 4가지 조합을 아래와 같이 Junit5 테스트 코드로 작성해봤다.
package hello.dailycode.rest;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.Map;
public class DoubleUrlEncodingTest {
private static final RestTemplate restTemplate
= new RestTemplate(new HttpComponentsClientHttpRequestFactory());
@Test
@DisplayName("방식1. UriComponentsBuilder.encode() 사용 + UriComponents.toUri() 사용")
void requestV1() {
UriComponents complexUrl = UriComponentsBuilder
.fromUriString("http://localhost:8080/good/morning/{requestId}")
.uriVariables(Map.of("requestId", "requestV1"))
.queryParam("userName", "한글이름")
.queryParam("userId", "dailyCode")
.encode().build();
restTemplate.getForEntity(complexUrl.toUri(), String.class);
}
@Test
@DisplayName("방식2. UriComponentsBuilder.encode() 미사용 + UriComponents.toUri() 사용")
void requestV2() {
UriComponents complexUrl = UriComponentsBuilder
.fromUriString("http://localhost:8080/good/morning/{requestId}")
.uriVariables(Map.of("requestId", "requestV2"))
.queryParam("userName", "한글이름")
.queryParam("userId", "dailyCode")
/*.encode()*/.build();
restTemplate.getForEntity(complexUrl.toUri(), String.class);
}
@Test
@DisplayName("방식3. UriComponentsBuilder.encode() 사용 + UriComponents.toUriString() 사용")
void requestV3() {
UriComponents complexUrl = UriComponentsBuilder
.fromUriString("http://localhost:8080/good/morning/{requestId}")
.uriVariables(Map.of("requestId", "requestV3"))
.queryParam("userName", "한글이름")
.queryParam("userId", "dailyCode")
.encode().build();
restTemplate.getForEntity(complexUrl.toUriString(), String.class);
}
@Test
@DisplayName("방식4. UriComponentsBuilder.encode() 미사용 + UriComponents.toUriString() 사용")
void requestV4() {
UriComponents complexUrl = UriComponentsBuilder
.fromUriString("http://localhost:8080/good/morning/{requestId}")
.uriVariables(Map.of("requestId", "requestV4"))
.queryParam("userName", "한글이름")
.queryParam("userId", "dailyCode")
/*.encode()*/.build();
restTemplate.getForEntity(complexUrl.toUriString(), String.class);
}
}
Spring Boot 를 실행시키고 나서 작성한 테스트를 한번에
실행시키면 아래와 같은 로그가 나온다.
================================requestV1================================
userName: [한글이름]
userId: [dailyCode]
================================requestV2================================
userName: [한글이름]
userId: [dailyCode]
================================requestV3================================
userName: [%ED%95%9C%EA%B8%80%EC%9D%B4%EB%A6%84]
userId: [dailyCode]
================================requestV4================================
userName: [한글이름]
userId: [dailyCode]
딱보면 알겠지만, requestV3
요청에 사용된 query parameter 에서 "한글이름" 이라는
한글 문자열이 깨졌다.
이게 바로 2번 url encoding에 의한 현상이다.
실제로 저게 2번 url encoding 이 되었다는 것은 어떻게 알 수 있을까?
이를 확인하기 전에 저 깨진 문자열이 나오게 된 경위를 파악하겠다.
2번
encode 되서 요청 전송 요청 전송1번
일어남즉, Server 에 도착한 깨진 문자열을 한번만 더 decode 를 하면 정상적인 값이 나온다는 뜻이다.
그렇다면 저 깨진 문자열을 한번 더 decode 를 해서 정상적인 "한글이름"이 출력되면
2번 url encoding 이 일어났다는 것이 증명되는 것이다.
확인을 위해서 이 웹사이트 접속해서 아래와 같이 간단한 테스트를 해봤다.
깨진 문자열을 복사, 붙여넣기 하고 decode
를 클릭하면...
정상적으로 "한글이름"이라고 나온다!
결론
이런 url encoding 중복 현상이 일어나지 않도록 될 수 있으면
UriComponentsBuilder.encode() 사용 + UriComponents.toUriString()
방식을 자제하자.
사실 위에서 아예 요청 자체가 안될 가능성이 있는 것도 하나 있다.
그건 바로 requestV2 이다. 만약 내가 아래처럼 하지 않고,
private static final RestTemplate restTemplate
= new RestTemplate(new HttpComponentsClientHttpRequestFactory());
요렇게 하고 요청을 보내면 어떨끼?
private static final RestTemplate restTemplate
= new RestTemplate();
//= new RestTemplate(new SimpleClientHttpRequestFactory()); 도 마찬가지
아래와 같은 에러 문구를 보인다.
23:40:22.805 [Test worker] DEBUG org.springframework.web.client.RestTemplate
- Response 400 BAD_REQUEST
나는 그냥 "아, 에러가 나는구나"하고 넘어가겠다.
이것에 대한 상세한 이유가 알고 싶다면... 개인적으로 알아내보시길 바랍니다!
아무튼 requestV2
방식도 Factory 를 뭘 쓰냐에 따라 될 수도 안될 수도 있으니 조심하자.
한번도 에러를 내지 않고, 한글 문자열 파라미터도 제대로 보냈던
requestV1, requestV4 를 쓰면 될 거 같다.
@Test
@DisplayName("방식1. UriComponentsBuilder.encode() 사용 + UriComponents.toUri() 사용")
void requestV1() {
UriComponents complexUrl = UriComponentsBuilder
.fromUriString("http://localhost:8080/good/morning/{requestId}")
.uriVariables(Map.of("requestId", "requestV1"))
.queryParam("userName", "한글이름")
.queryParam("userId", "dailyCode")
.encode().build();
restTemplate.getForEntity(complexUrl.toUri(), String.class);
}
@Test
@DisplayName("방식4. UriComponentsBuilder.encode() 미사용 + UriComponents.toUriString() 사용")
void requestV4() {
UriComponents complexUrl = UriComponentsBuilder
.fromUriString("http://localhost:8080/good/morning/{requestId}")
.uriVariables(Map.of("requestId", "requestV4"))
.queryParam("userName", "한글이름")
.queryParam("userId", "dailyCode")
/*.encode()*/.build();
restTemplate.getForEntity(complexUrl.toUriString(), String.class);
}
사실 위에 저렇게 결론을 냈지만, 결국은 자기가 어떤 Factory 를 RestTemplate 에 제공하는지, 어떤 Interceptor 를 제공하는지에 따라서 또 바뀔 수 있다.
그러니 자기가 일하는 곳에서 내가 작성한 테스트 코드를 가져다가 한번 테스트를 실행해보고 결론을 내리자.
도움이 됐습니다