스프링 공부 (인프런 김영한 선생님)/스프링 MVC 2편

[스프링 웹 MVC 2편] 004. 국제화 메세지

ProgYun. 2023. 8. 11. 17:59

특정 단어를 모두 다른 단어로 바꾸고 싶으면 모든 곳을 수정해야하는 난관에 봉착할 수도 있다?!

수십개의 파일을 모두 고치려면 머리아프다

HTML에 하드코딩되어있기 때문인데 -> 좀 더 편하게 하는 방법이 없을까? // 메세지 기능을 사용하자!

별도로 만들 필요 없음 - 설정까지 다 되어있음 스프링 부트에서는

메세지 - 인터페이스
setBasenames - 읽을 파일
defaultEncoding - 인코딩 형식


스프링 메세지 소스 설정

위에서 아주 간략하게 설명한 스프링 메세지를 사용하려면
스프링 부트는 해당 기능이 이미 빈으로 등록되어 있어 application.properties에 간단하게 다음 내용의 등록을 통해 사용할 수 있으나, 단순 스프링이라면 다음과 같이 스프링 빈을 등록해주어야 한다.

@Bean
public MessageSource messageSource(){
    ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasenames("messages", "error");
        messageSource.setDefaultEncoding("utf-8");
        return messageSource;
}

basenames : 설정 파일의 이름
defaultEncoding : 인코딩 정보를 지정한다.

messages.properties 파일

label.item=상품  
label.item.id=상품 IDlabel.item.itemName=상품명  
label.item.price=가격  
label.item.quantity=수량  
page.items=상품 목록  
page.item=상품 상세  
page.addItem=상품 등록  
page.updateItem=상품 수정  
button.save=저장  
button.cancel=취소

messages_en.properties 파일

label.item=Item  
label.item.id=Item ID  
label.item.itemName=Item Name  
label.item.price=price  
label.item.quantity=quantity  
page.items=Item List  
page.item=Item Detail  
page.addItem=Item Add  
page.updateItem=Item Update  
button.save=Save  
button.cancel=Cancel

다음과 같이 작성하면 Locale 정보에 따라 위의 properties 파일이 기본값으로 동작하나
Locale 정보가 en-us 등 으로 오는 경우, messages_en.properties에 기술된 내용으로 html이 렌더링된다


스프링 메세지 소스 사용

MessageSource 인터페이스는 다음과 같이 기술되어 있다

public interface MessageSource {  

    /**  
     * Try to resolve the message. Return default message if no message was found.     * @param code the message code to look up, e.g. 'calculator.noRateSet'.  
     * MessageSource users are encouraged to base message names on qualified class     * or package names, avoiding potential conflicts and ensuring maximum clarity.     * @param args an array of arguments that will be filled in for params within  
     * the message (params look like "{0}", "{1,date}", "{2,time}" within a message),     * or {@code null} if none  
     * @param defaultMessage a default message to return if the lookup fails  
     * @param locale the locale in which to do the lookup  
     * @return the resolved message if the lookup was successful, otherwise  
     * the default message passed as a parameter (which may be {@code null})  
     * @see #getMessage(MessageSourceResolvable, Locale)  
     * @see java.text.MessageFormat  
     */    @Nullable  
    String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);  

    /**  
     * Try to resolve the message. Treat as an error if the message can't be found.     * @param code the message code to look up, e.g. 'calculator.noRateSet'.  
     * MessageSource users are encouraged to base message names on qualified class     * or package names, avoiding potential conflicts and ensuring maximum clarity.     * @param args an array of arguments that will be filled in for params within  
     * the message (params look like "{0}", "{1,date}", "{2,time}" within a message),     * or {@code null} if none  
     * @param locale the locale in which to do the lookup  
     * @return the resolved message (never {@code null})  
     * @throws NoSuchMessageException if no corresponding message was found  
     * @see #getMessage(MessageSourceResolvable, Locale)  
     * @see java.text.MessageFormat  
     */    String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;  

    /**  
     * Try to resolve the message using all the attributes contained within the     * {@code MessageSourceResolvable} argument that was passed in.  
     * <p>NOTE: We must throw a {@code NoSuchMessageException} on this method  
     * since at the time of calling this method we aren't able to determine if the     * {@code defaultMessage} property of the resolvable is {@code null} or not.  
     * @param resolvable the value object storing attributes required to resolve a message  
     * (may include a default message)     * @param locale the locale in which to do the lookup  
     * @return the resolved message (never {@code null} since even a  
     * {@code MessageSourceResolvable}-provided default message needs to be non-null)  
     * @throws NoSuchMessageException if no corresponding message was found  
     * (and no default message was provided by the {@code MessageSourceResolvable})  
     * @see MessageSourceResolvable#getCodes()  
     * @see MessageSourceResolvable#getArguments()  
     * @see MessageSourceResolvable#getDefaultMessage()  
     * @see java.text.MessageFormat  
     */    String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;  

}

해당 내용을 보면 코드를 포함하여 일부 파라미터를 메세지로 읽어오는 것을 볼 수 있는데

package hello.itemservice.message;  

import org.assertj.core.api.Assertions;  
import org.junit.jupiter.api.Test;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.boot.test.context.SpringBootTest;  
import org.springframework.context.MessageSource;  
import org.springframework.context.NoSuchMessageException;  

import java.util.Locale;  

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;  
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;  

@SpringBootTest  
public class MessageSourceTest {  

    @Autowired  
    MessageSource messageSource;  

    @Test  
    void helloMessage(){ 
        Locale.setDefault(Locale.KOREA);  
        String result = messageSource.getMessage("hello", null, null);  
        Assertions.assertThat(result).isEqualTo("안녕");  
        // 로케일 정보가 없으면 basename에서 설정한 기본 이름 메세지 파일을 조회
        // basename으로 messages를 지정했으므로, messages.properties 파일에서 데이터 조회
    }  

    // 메세지가 없는 경우 기본 메세지
    @Test  
    void notFoundMessageCode(){ 
        assertThatThrownBy(() -> messageSource.getMessage("no_code", null, null))  
                .isInstanceOf(NoSuchMessageException.class);  
    }  

// 메세지가 없는 경우, NoSuchMessageException이 발생한다

    @Test  
    void notFoundMessageCodeDefaultMessage(){  
        String result = messageSource.getMessage("no_code", null, "기본 메세지" ,null);  
        assertThat(result).isEqualTo("기본 메세지");  
    }  
// 메세지가 없어도 기본 메세지를 사용하면 기본 메세지가 반환된다.

    @Test  
    void argumentMessage(){  
        Locale.setDefault(Locale.KOREA);  
        String message = messageSource.getMessage("hello.name", new Object[]{"Spring"}, null);  
        assertThat(message).isEqualTo("안녕 Spring");  

    }  
    // 매개변수 사용, {0} 부분을 전달해서 사용할 수 있음 (단, 배열로 넘겨야 함.)
}

웹 애플리케이션에 메세지 적용하기

위에서 등록한 messages.propertiesmessages_en.properties를 기준으로 설명하겠다.

타임리프에서 #{...}을 사용하면 스프링의 메세지를 편리하게 조회할 수 있다.
예를 들어서 방금 등록한 상품이라는 이름을 조회하려면 #{label.item} 식으로 기술한다.

  • 렌더링 전
    <h2 th:text="#{page.addItem}">상품 등록 폼</h2>
  • 렌더링 후
    <h2>상품 등록</h2>

웹 애플리케이션에 국제화 적용하기

Accept-Language에 따라 국제화를 적용하면 언어를 다르게 표시할 수 있다
이 때 Accept-Language의 경우 웹 브라우저 설정에 따라 결정된다.

MessageSource는 기본적으로 Locale 정보를 알아야 언어 선택이 가능하다
이 헤더값을 사용하거나 스프링은 Locale 선택 방식을 변경할 수 있도록 LocaleResolver라는 인터페이스를 제공한다.

기본은 Accept-Language를 활용하는 AcceptHeaderLocaleResolver를 사용한다.

public interface LocaleResolver {  

       Locale resolveLocale(HttpServletRequest request);  

       void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale);  

}

Locale 선택 방식 변경을 위해서는 LocaleResolver의 구현체를 변경해서 쿠키/세션 기반의 Locale 선택 기능을 사용할 수 있다.