개발관련 잡다/HTTP

http와 스프링 (5) : 다국어 API, 페이지에 대한 캐싱고민과 300 응답?

아라한사 2019. 11. 10. 13:17

 

[이번에는 정말 쉬어가기 및 욕심편]

[그냥 이번편은 이런 것이 있다라기보다는 그냥 생각의 주저리주저리..랄까요]

 

시리즈목차 

https://adunhansa.tistory.com/261

 

http와 스프링(0) - 연재를 시작하며..

운 좋게, 좋은 스터디원분들과 좋은 책을 만나 HTTP 스터디를 시작하며 다음의 연재글을 시작해보려고 합니다. 목차 http와 스프링 뒤적뒤적 연재 시리즈~ --- 1편 - Encoding - Brotli 적용해보기 https://adunha..

adunhansa.tistory.com

 

지난 3편에서 api, html 페이지까지의 캐싱을 알아보았습니다. 

결론은  URL 에서의 캐싱전략을 잘 사용하자인데 여기서 한가지 고민이 생기게 마련입니다.

URL은 같지만, Accept-Language 속성으로 변하는 URL들은 어떻게 해야할까요?!

 

0. 개요 Accept-Language 속성과 다국어 페이지의 등장

 

들어가기전에 한번 스윽 살펴봐주시고~

 

Acccpet-Language
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language

https://www.zerocho.com/category/HTTP/post/5b3ba2d0b3dabd001b53b9db

 

 

자 다시 돌아와서.. 다국어로 콘텐츠를 돌려주는 다음의 HTML 페이지가 있다고 가정을 해보겠습니다.

깐트롤러

@GetMapping("/html")
    fun citiesHtml(webRequest: WebRequest, model: Model) : String?{
        val eTag: Long = getEtag()
        if(webRequest.checkNotModified(eTag)) return null
        model.addAttribute("cities", City.values())
        return "chap2/cities"
    }

이늄

enum class City(val key: String) {
    SEOUL("seoul"),
    INCHOEN("incheon"),
    CHEONGJU("cheongju");
}

HTML 

<body>
<h1>도시목록</h1>
<span th:each="city : ${cities}" th:text="#{${city.key}}"></span>
</body>

messages.properties 예시

seoul=Seoul
incheon=Incheon
cheongju=Cheongju

이런 응답이 올텐데.. 사용자의 Locale 에 따라 언어가 다르게 나타나지게 될 것입니다. 

 

스프링에서는 로케일을 다루는 여러 LocaleResolver 가 있습니다. 

 

https://gs.saro.me/dev?page=11&tn=488

 

스프링 i18n (다국어) : 2. Locale Resolver - 가리사니

# 스프링 i18n (다국어) 시리즈 - [1. 기본설정 및 타임리프에 적용](/dev?tn=487) - [2. Locale Resolver](/dev?tn=488) # Locale Resolver 전장의 Bean 인 localeResolver 에 대해서 알아보겠습니다. ``` java @Bean public LocaleResolver localeR...

gs.saro.me

기본으로는 Accept-Language 정보를 읽는 AcceptHeaderLocaleResolver 을 사용하기 때문에

미국이나 한국 사용자가 접속해도 자기언어만 고정되어서 잘 나오게 될 것입니다. 

 

하지만 Accept-Language 에 따라서 같은 주소가 뭔가 다른 캐시 정보를 가진다는 것이

개발자의 욕심(?)에서 뭔가 다른 전략을 세워보고 싶습니다.

1.  다국어 URL 정책? 과 구현?

그리하여 이런 느낌의 HTML 페이지 주소정책을 가져보겠습니다.

html-gateway 로 접근시  접근하는 Locale 정보를 읽어서

html.ko 혹은 html.en 등으로 분산할 것입니다. 

 

게이트웨이에서의 응답코드에서는 307 응답코드를 반환하겠습니다.

(추가 수정사항 : Vary , Content-Language 정보를 직접 주기 위하여 redirectView.render 를 사용하였습니다

 

@GetMapping("/html-gateway")
    fun citiesHtml(locale: Locale, request : HttpServletRequest, response: HttpServletResponse) {
        println("display lang : ${locale.language}")
        val redirectView = RedirectView("/chap2/html.${locale.language}")
        redirectView.setStatusCode(HttpStatus.TEMPORARY_REDIRECT)
        response.setHeader("Vary", "Accept-Language")
        response.setHeader("Content-Language", locale.language)
        redirectView.render(null, request, response)
    }

https://docs.oracle.com/javase/7/docs/api/java/util/Locale.html

 

Locale (Java Platform SE 7 )

getDisplayLanguage public final String getDisplayLanguage() Returns a name for the locale's language that is appropriate for display to the user. If possible, the name returned will be localized for the default locale. For example, if the locale is fr_FR a

docs.oracle.com

locale 은 en-US , en 이렇게 정보를 가질 수 있기 때문에 language 를 하면 앞의 소문자만 가져오는 것으로 해서.. 

 

@GetMapping("/html.{lang}")
    fun citiesHtmlByLang(webRequest: WebRequest, @PathVariable lang: String): ModelAndView? {

다음의 html.ko , html.en 같은 주소가 다국어 페이지를 받을 수 있도록 처리해보겠습니다.

그리고 조금 더 욕심이 나는 것이 있다면 .. 지원하지 않는 언어일 때는 어떻게 될까요?

 

2. 분기처리에 따른 300 응답 코드의 등장

 

혹시나 손가락에 기름을 발라 미끌어지거나, 실수로 오타를 치고 온 사람일 수있으니 

불친절한 404보다는 친절한 300 Multiple Choice 상태코드와 함께

가능한 언어세트 링크를 보여주면 더 친절하지않을까요? 

 

HTTP 책을 공부하다가 300 응답코드를 보면서 누가 이런걸 구현했어? 라고 생각을 했는데

마침 당일날 제가 300코드를 보고 말았습니다;; (머릿속으로 생각하니 나오는 인과의 흐름인가요..)

 

뭔가 나도 이 페이지를 구현하고 싶긴한데..ㅇ.ㅇa 라는 욕망이 드는 순간입니다.

 

다시 한번 본래의 목적의 흐름으로 돌아와서.. 

 

첫번째 -  어떤 페이지에 대한 요청 흐름을 -> 다국어 페이지로 분기한다. 

두번째 -  분기된 페이지에서 다국어 파라미터를 검증하는데 지원하지 않는 언어인 경우 멀티플 페이지를 보여준다

를 상기해보면서 가보겠습니다.

 

위에서 정의한 분기받는 컨트롤러에 다음과 같이 정의를 해주겠습니다. 

(변수순서는 여기서 따지지 않기로 합니다^0^)

 

대략 완성은 여기에.. https://github.com/arahansa/learn_http_with_spring/blob/master/first/src/main/kotlin/com/arahansa/chapter2/AcceptLanguageController.kt#L55

val supportLangs = listOf("ko", "en")
        val mav = ModelAndView()
        // 욕심코드.. 그냥 404 때려도..^0^?
        if (!supportLangs.contains(lang)) {
            mav.addObject("langs", supportLangs.map { lang -> Locale(lang) })
            mav.addObject("lang", lang)
            mav.status = HttpStatus.MULTIPLE_CHOICES
            mav.viewName = "chap2/multiple"
        } else {

받은 파라미터를 가지고서 multiple 이라는 페이지로 분기시키면서 300 응답 코드로 뷰 렌더링을 하게 했습니다. 

응답 HTML 은 다음과 같습니다. 

<body>

<img src="/img/dudum.png">
<h1>당신은 오타를 낸 것이 분명해..</h1>

<span th:text="${lang}"></span><span>는 지원하지 않는 언어표현입니다.</span><br>
<span>다음의 페이지중 하나를 들어가기를 권장합니다.</span><br><br>

<div th:each="lang : ${langs}">
    <a th:href="@{'/chap2/html.'+${lang.language}}" th:text="${'/chap2/html.'+lang.language}"></a>
    <span th:text="${lang.displayLanguage}"></span><span>페이지</span>
    <br>
</div>

</body>

자 그럼 눈으로 한번 보기를 해볼까요? 이렇게 한번 접근을 하면

 

주소는 제가 잠시 html-gateway 로 한 것인데 html.ko 로 리다이렉션되어져서 보여지게 됩니다. 

자 그러면 지원하지 않는 언어인 뚫훍뚫훍 언어로 접속해보겠습니다. 언어세트가 실수인것치고는 너무 구체적인거 아냐? 하시겠는데 기분탓입니다.

 

결과는 친절하게도 300응답 코드와 함께 안내 페이지가 떴군요. 

조금 만족스럽긴한데 과연 누가 이런 페이지 올까.. 의문스럽습니다.

서버는 이렇게 여러 언어에 대한 기능을 제공하지만,

뷰 렌더링으로 화면을 제공한다고 해도 클라이언트는 이러한 정보를 모두 모를 수가 있습니다.

ko, en 에서 jp 가 있는지 모릅니다. 이러한 특성을 URI 불투명성이라고 하던데 자세한 부분은 생략;;

 

---

3. 동적 콘텐츠 채우기 (+다국어)의 API 화

 

자 그러면 링크에 나온 en 도 접속해볼까요.. 

띠용?  이게 뭔가 싶긴한데 맞는 결과화면입니다. 현재의 로케일 리졸버는 브라우저의 헤더를 읽는 AcceptHeaderLocaleResolver 이기 때문입니다.

 

이렇게 하면.. 영어로 나오긴하는데.. -_- 이와 같다면 주소는 en 으로 끝나도 헤더를 ko 로 붙이면 한국어로 나오게 되겠죠.. 다음 절에서 조금 더 변환을 해보겠습니다. 

thymeleaf 의 #{다국어키}를 위해서라도, 사실 AcceptHeaderLocaleResolver 말고 다른 로케일리졸버를 사용하는게 편합니다만,  그 부분은 논외로 치고 저기 나오는 도시 목록을 동적컨텐츠로 생각하고 저 도시 목록부분을 ajax 로딩으로 바꿔보겠습니다.

 

동적콘텐츠부분을 ajax 형태의 리로딩

 

윗 HTML 느낌 비슷하게 api 하나 만들어보았습니다. pathvariable 인자로 받은 lang 파라미터를 가지고서 locale 을 만들어주겠습니다.

@GetMapping("/cities.{lang}")
    @ResponseBody
    fun citiesByLang(req: HttpServletRequest, @PathVariable lang: String) : ResponseEntity<List<String>>{
        val cityList = City.values().toList().map { messageSource.getMessage(it.key, null, Locale(lang)) }
        return ResponseEntity.ok()
                .eTag(cityList.hashCode().toString())
                .body(cityList)
    }

컨트롤러 뷰 응답부분에서는 다음과 같이 적어볼것입니다.

lang 속성을 javascript 에서 다시 읽게 해봐야겠네요. AcceptHeaderLocaleResolver 때문에 저렇게 하는거지 

다른 리졸버를 설정하고 html lang 속성에 로케일 랭기지를 설정하는 방법도 있습니다.

if (!supportLangs.contains(lang)) {
            mav.addObject("langs", supportLangs.map { lang -> Locale(lang) })
            mav.addObject("lang", lang)
            mav.status = HttpStatus.MULTIPLE_CHOICES
            mav.viewName = "chap2/multiple"
        } else {
            mav.addObject("lang", lang)
            mav.addObject("cities", City.values())
            mav.viewName = "chap2/cities-ajax"
            mav.status = HttpStatus.OK
        }

그리고 화면단에서는 lang 을 다시 모델로 받아서 ajax 호출을 할 것입니다. 

타임리프 화면단에서 인라인 변수를 설정하는 점은 여기를 참조했습니다. 

https://www.concretepage.com/thymeleaf/thymeleaf-javascript-inline-example-with-variable

 

샘플코드라는 점..(^^)

<script>
    /*<![CDATA[*/
    const lang = /*[[${lang}]]*/ 'lang';
    $.get(`/chap2/cities.${lang}`, data => {
        $("#target").html(Array.from(data).join(","))
    })
    /*]]>*/
</script>

자 그럼 다시 화면을 보겠습니다.

게이트웨이로 접근해봐야 한국어만 나오기 때문에 en 으로 직접 쳐보겠습니다.

 

캐시 리프레시하고 첫번째 접속시 화면입니다.  200응답 코드와 함께 ajax 로 cities.en 을 호출하고 내용을 채워서 화면이 만들어집니다. 

 

재접속시의 화면입니다. 

화면렌더링도 304 로 떨어지고, 동적 컨테츠를 채울 도시목록도 304 로 떨어졌습니다.

 

여기까지를 통하여

Http와 스프링 (3) : eTag를 이용하여 View 에도 적용하기 ( https://adunhansa.tistory.com/258 )
에서의 추가고민 사항이었던 다국어 렌더링과 동적콘텐츠에 대한 고민을 잠시 해결해보는 시간을 가져보았습니다.

 

감사합니다.