본문 바로가기

스프링

스프링 UriComponentsBuilder로 공공데이터포탈 키 인코딩하기

이상한거로 3시간 잡아먹었다

결론과 코드는 맨밑이니 걍 맨밑에가서 메서드복사해다가 써도됨

 

기본적으로 https://datatracker.ietf.org/doc/html/rfc3986#section-2.2 에 규정된대로,/나 +등 예약된 문자들은 따로 인코딩하지않아도 url에 사용할수있고,그래서 UriComponentsBuilder는 저런값들을 인코딩하지 않는다

근데 문제는 공공데이터포탈의 키는 이걸 인코딩해야한다

문제는 더있는데,만약 그래서 인코딩된 키를 사용하려고 하면,인코딩이 됐으니 %가 들어있어서 다시 인코딩이 된다

 

GTFN0/s1 이게 인코딩하기전,즉 디코딩키라면

GTFN0%2Fs1 정상인코딩은 이거고

GTFN0%252Fs1이중인코딩은 이거다

 

즉 %가 두번 인코딩되면 %25가 추가되는거다(  / -> %2F -> %252F )

 

그래서 이럴땐 아예 인코딩을 꺼버리고 사용하면된다,

즉 인코딩을 끄고 미리 인코딩된 키를 사용하면 된다

 

문제는 쿼리파라미터로 한글을 사용해야할경우다

이러면 한글이 들어가는 모든 쿼리파라미터를 따로 인코딩해야한다(실수하기 딱좋음)

 

즉 한글+"/"가 들어가면

인코딩된 키로 인코딩을 한다->이중인코딩으로 키 에러

디코딩된 키로 인코딩을 한다->키가 인코딩되지않아 키 에러

인코딩된 키로 인코딩을 하지않는다->한글이 인코딩되지않아 에러

디코딩된 키로 인코딩을 하지않는다-> 키가 인코딩되지않아 키 에러

 

이렇게 사분면 전부 에러가 나는 대참사가 나게된다

 

즉 결론은 우리는 결국 수동으로 인코딩을 하고 인코딩을 꺼야한다

문제는 클라이언트에 인코딩을 맞기면 딱 실수하기좋고,이건 api를 처리하는쪽에서 처리해야할문제라는거다

애초에 응집도나 책임으로 봐도 웹관련 책임이 밖으로 새어나가는거부터 맘에안들기도 하니까 말이다

 

또한 이건 저 공공데이터포탈의 이상동작에 가까우니,전역api통신을 담당하는 클래스가 있다면 해당 객체에 넣는거도    책임이 유출된다,즉 최외곽 어댑터,공공데이터포탈 통신어댑터에 이 로직이 들어가는게 맞다

 

그래서 어떻게 처리했냐면

전역api통신 인터페이스에

fun buildUrl(
    baseUrl: String,
    queryParams: MultiValueMap<String, String>?,
    encoded: Boolean = false,
): URI

이렇게 encode를 미리 했는지 체크하는 매개변수를 추가하고,기본값은 인코딩을 자동으로하는 false로 뒀다(미리 인코딩을 하지않았다는뜻)

그리고 구현체에

override fun buildUrl(
    baseUrl: String,
    queryParams: MultiValueMap<String, String>?,
    encoded: Boolean,
): URI =
    UriComponentsBuilder
        .fromHttpUrl(baseUrl)
        .queryParams(queryParams)
        .build(encoded)
        .toUri()

이렇게 build에 encoded로 인코딩을 할지말지를 체크했고(false면 UriComponentsBuilder가 인코딩을 하고,

true면 UriComponentsBuilder가 인코딩을 하지않음)

이걸 가져다쓰는곳(즉 공공데이터포탈 통신어댑터)에서

//import org.apache.catalina.util.URLEncoder  이거사용
// 오픈api용 인코딩,/나 +같은걸 한글과 같이 인코딩하기위해 필요
private fun specialEncode(queryParams: MultiValueMap<String, String>?): MultiValueMap<String, String> {
    val encodeQueryParams = LinkedMultiValueMap<String, String>()
    val urlEncoder = URLEncoder()
    if (queryParams != null) {
        for ((key, valueList) in queryParams) {
            val resValueList = mutableListOf<String>()
            for (value in valueList) {
                resValueList.add(urlEncoder.encode(value, Charset.forName("UTF-8")))
            }
            encodeQueryParams.addAll(key, resValueList)
        }
    }
    return encodeQueryParams
}

저렇게 아파치의 URLEncoder을 사용해서 인코딩을 수동으로 처리하고(저건 /나 +를 다 인코딩해줌,코드짜서 만들려다가 저거 찾아서 저거로 처리했음)

override fun fetch(queryKey: String): Map<String, String> {
    val restTemplate = apiHelper.createRestTemplate()

    val queryParams = createQueryParams(queryKey, ApiConfig.getOpenApiKey())
    val specialEncode = specialEncode(queryParams)

    val url = apiHelper.buildUrl(getBaseUrl(), specialEncode, true)

    return apiCall(restTemplate, url, queryKey)
}

해당 코드를 사용하기전에 수동으로 인코딩하고,true를 넣어서 미리 인코딩했으니 인코딩하지말아달라고 보내는식으로 처리했다

 

진짜 개발하다보면 별에별일이 다생긴다 ㅋㅋ