JiwonDev

Spring ๋ฉ”์‹œ์ง€, ๊ตญ์ œํ™” ๊ธฐ๋Šฅ

by JiwonDev

๋ฉ”์‹œ์ง€์™€ ๊ตญ์ œํ™” ๊ธฐ๋Šฅ์€ ์Šคํ”„๋ง MVC๋งŒ ์‚ฌ์šฉํ•ด์„œ [MessageSource ์Šคํ”„๋ง ๋นˆ]์„ ์ง์ ‘ ๊ตฌํ˜„ํ•ด๋„ ๋˜์ง€๋งŒ, ์Šคํ”„๋ง๊ณผ ํ‹ฐ์ž„๋ฆฌํ”„์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ตญ์ œํ™”์™€ ๋ฉ”์‹œ์ง€ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋ฉด ์ƒ๋‹นํžˆ ํŽธ๋ฆฌํ•˜๋‹ค.

/* messageSource ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ด์šฉํ•ด์„œ ์Šคํ”„๋ง์˜ ๋ฉ”์‹œ์ง€ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. */

@Bean // ์Šคํ”„๋ง๋ถ€ํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋”ฐ๋กœ ๋“ฑ๋กํ•˜์ง€ ์•Š์•„๋„ ์ž๋™์œผ๋กœ ๋“ฑ๋ก๋˜์–ด์žˆ๋‹ค.
public MessageSource messageSource() {
	ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
	messageSource.setBasenames("messages", "errors");
	messageSource.setDefaultEncoding("utf-8");
	return messageSource;
}

MessageSource ๊ฐ์ฒด ์„ค์ • ๋ฐฉ๋ฒ•, ์Šคํ”„๋ง๋ถ€ํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์•Œ์•„์„œ ํ•ด์ค€๋‹ค.

// application.properties
spring.messages.basename=messages // messageSource ๊ฐ์ฒด์˜ ๋นˆ ์ด๋ฆ„. ๊ธฐ๋ณธ๊ฐ’์ž„

 

application.properties ์˜ต์…˜๊ฐ’ ์•Œ์•„๋ณด๊ธฐ (Spring Boot 2.5.4)

 

Common Application Properties

 

docs.spring.io

 


# ๋ฉ”์‹œ์ง€ ๊ธฐ๋Šฅ์ด๋ž€

HTML ๋ทฐ์— ์ ํ˜€์žˆ๋Š” '์ƒํ’ˆ๋ช…' ์ด๋ผ๋Š” ๊ธ€์ž๋ฅผ ์ „๋ถ€ '์ƒํ’ˆ์ด๋ฆ„'์œผ๋กœ ๋ฐ”๊พธ๋ ค๊ณ  ํ•˜๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผํ• ๊นŒ?

์ด๋Ÿฐ ์ด๋ฆ„๋“ค์„ Message๋ผ๊ณ  ๋ถ€๋ฅด๊ณ , ๋‹ค์–‘ํ•œ Message๋“ค์„ ํ•˜๋‚˜๋กœ ๊ด€๋ฆฌํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๋ฉ”์‹œ์ง€ ๊ธฐ๋Šฅ์ด๋ผ๊ณ  ํ•œ๋‹ค.

 

์˜ˆ๋ฅผ ๋“ค์–ด message.properteis๋ผ๋Š” ํŒŒ์ผ์„ ๋งŒ๋“ค๊ณ  ์•„๋ž˜์™€ ๊ฐ™์ด ๋ฉ”์‹œ์ง€๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

item=์ƒํ’ˆ
item.id=์ƒํ’ˆ ID
item.itemName=์ƒํ’ˆ๋ช…
item.price=๊ฐ€๊ฒฉ
item.quantity=์ˆ˜๋Ÿ‰

 


# ๊ตญ์ œํ™” ๊ธฐ๋Šฅ์ด๋ž€

HTTP์˜ accpet=language ํ—ค๋”๊ฐ’๋“ฑ์œผ๋กœ ์‚ฌ์šฉ์ž์˜ ์–ธ์–ด ์ง€์—ญ(Locale)์„ ์•Œ์•„๋‚ด์„œ ์–ธ์–ด๋ณ„๋กœ ๋ฉ”์‹œ์ง€ ํŒŒ์ผ(.properteis)์„ ๋‹ค๋ฅด๊ฒŒ ์ ์šฉ์‹œํ‚ค๋Š” ๊ธฐ๋Šฅ์ด๋‹ค.

์‚ฌ์šฉ์ž์˜ ์–ธ์–ด๋ฅผ ํŒŒ์•…ํ•ด์„œ ๋ฉ”์‹œ์ง€ ์„ค์ •ํŒŒ์ผ์„ ์ ์ ˆํ•˜๊ฒŒ ๋ณ€๊ฒฝํ•˜๋Š” ๊ธฐ๋Šฅ

 


# ์Šคํ”„๋ง, ํƒ€์ž„๋ฆฌํ”„์— ๋ฉ”์‹œ์ง€ ์ ์šฉํ•˜๊ธฐ

์Šคํ”„๋ง์€ ์•„๋ž˜์™€ ๊ฐ™์€ ๊ธฐ๋ณธ์œผ๋กœ ๋ฉ”์‹œ์ง€ ์ด๋ฆ„(messages) ์„ค์ • ๊ฐ’์„ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค.

  • resources/ ์•ˆ์— ์žˆ๋Š” messages.properties, messages_ko.properties ๋“ฑ์„ ์ž๋™์œผ๋กœ ์ธ์‹ํ•œ๋‹ค.
// application.properties
spring.messages.basename=messages // messageSource ๊ฐ์ฒด์˜ ๋นˆ ์ด๋ฆ„. ๊ธฐ๋ณธ๊ฐ’์ž„
messages ๋ผ๋Š” ๋นˆ์„ ๋“ฑ๋กํ–ˆ๋‹ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์€ ํŒŒ์ผ์„ ์ž๋™์œผ๋กœ ์ธ์‹ํ•œ๋‹ค.

- messages_en.properties
- messages_ko.properties
- messages_๊ตญ๊ฐ€์ฝ”๋“œ.properties
- messages.properties (default ๋ฉ”์‹œ์ง€ ์„ค์ •๊ฐ’)

 

  • message.properties๋ฅผ ์•„๋ž˜์™€ ๊ฐ™์ด ์ˆ˜์ •ํ•ด๋ณด์ž.
# message.properties
hello=์•ˆ๋…•
hello.name=์•ˆ๋…• {0}

 

  • ์Šคํ”„๋ง์—์„œ๋Š” MessageSource ์ธํ„ฐํŽ˜์ด์Šค์— ์Šคํ”„๋ง ๋นˆ์„ ์ฃผ์ž…๋ฐ›์•„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
@SpringBootTest
public class MessageSourceTest {

  @Autowired
  MessageSource ms;

  @Test
  @DisplayName("hello ํ”„๋กœํผํ‹ฐ์—๋Š” [์•ˆ๋…•] ๊ฐ’์ด ๋“ค์–ด์žˆ๋‹ค.")
  void helloMessage() {
    String result = ms.getMessage("hello", null, null);
    assertThat(result).isEqualTo("์•ˆ๋…•");
  }

  @Test
  @DisplayName("ํ”„๋กœํผํ‹ฐ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ NoSuchMessageException์„ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค.")
  void notFoundMessageCode() {
    assertThatThrownBy(() -> ms.getMessage("no_code", null, null))
        .isInstanceOf(NoSuchMessageException.class);
  }

  @Test
  @DisplayName("ms.getMessage()๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํ”„๋กœํผํ‹ฐ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ ๊ธฐ๋ณธ๊ฐ’์„ ์ง€์ •ํ•ด์ค„ ์ˆ˜ ์žˆ๋‹ค.")
  void notFoundMessageCodeDefaultMessage() {
    String result = ms.getMessage("no_code", null, "๊ธฐ๋ณธ ๋ฉ”์‹œ์ง€", null);
    assertThat(result).isEqualTo("๊ธฐ๋ณธ ๋ฉ”์‹œ์ง€");
  }

  @Test
  @DisplayName("๋ฉ”์‹œ์ง€ ํŒŒ๋ผ๋ฉ”ํƒ€๋Š” Object[]๋กœ ์ค„ ์ˆ˜ ์žˆ๋‹ค.")
  void argumentMessage() {
    String message = ms.getMessage("hello.name", new Object[]{"Spring"}, null);
    assertThat(message).isEqualTo("์•ˆ๋…• Spring");
  }
  
  @Test
  @DisplayName("์ง€์—ญ(Locale)๊ฐ’์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ๋ฉ”์‹œ์ง€ ํ”„๋กœํผํ‹ฐ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.")
  void enLang() {
    assertThat(ms.getMessage("hello", null, Locale.ENGLISH)).isEqualTo("hello");
  }


  @Test
  @DisplayName("ํ•ด๋‹น ์ง€์—ญ(Locale)์ด ์—†๊ฑฐ๋‚˜ null์„ ์ฃผ๋ฉด ๊ธฐ๋ณธ ๋ฉ”์‹œ์ง€ ํ”„๋กœํผํ‹ฐ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.")
  void defaultLang() {
    assertThat(ms.getMessage("hello", null, null)).isEqualTo("์•ˆ๋…•");
    assertThat(ms.getMessage("hello", null, Locale.KOREA)).isEqualTo("์•ˆ๋…•");
  }
}

 

  • ํƒ€์ž„๋ฆฌํ”„์—์„œ๋Š” #{hello} #{hello.name('ํŒŒ๋ผ๋ฉ”ํƒ€')} #{ hello.name(${name}) } ์œผ๋กœ ์‚ฌ์šฉํ•œ๋‹ค.

์‹ค์ œ ํƒ€์ž„๋ฆฌํ”„์—์„œ ๋ฉ”์‹œ์ง€ ์‚ฌ์šฉ ์˜ˆ์ œ ์‚ดํŽด๋ณด๊ธฐ

๋”๋ณด๊ธฐ
# resources/messages.properties

label.item=์ƒํ’ˆ
label.item.id=์ƒํ’ˆ ID
label.item.itemName=์ƒํ’ˆ๋ช…
label.item.price=๊ฐ€๊ฒฉ
label.item.quantity=์ˆ˜๋Ÿ‰

page.items=์ƒํ’ˆ ๋ชฉ๋ก
page.item=์ƒํ’ˆ ์ƒ์„ธ
page.addItem=์ƒํ’ˆ ๋“ฑ๋ก
page.updateItem=์ƒํ’ˆ ์ˆ˜์ •

button.save=์ €์žฅ
button.cancel=์ทจ์†Œ
  <div>
    <label for="itemId" th:text="#{label.item.id}">์ƒํ’ˆ ID</label>
    <input class="form-control" id="itemId" name="itemId" readonly th:value="${item.id}"
           type="text" value="1">
  </div>
  <div>
    <label for="itemName" th:text="#{label.item.itemName}">์ƒํ’ˆ๋ช…</label>
    <input class="form-control" id="itemName" name="itemName" readonly th:value="${item.itemName}"
           type="text" value="์ƒํ’ˆA">
  </div>
  <div>
    <label for="price" th:text="#{label.item.price}">๊ฐ€๊ฒฉ</label>
    <input class="form-control" id="price" name="price" readonly th:value="${item.price}"
           type="text" value="10000">
  </div>
  <div>
    <label for="quantity" th:text="#{label.item.quantity}">์ˆ˜๋Ÿ‰</label>
    <input class="form-control" id="quantity" name="quantity" readonly th:value="${item.quantity}"
           type="text" value="10">
  </div>

 

 


# ์Šคํ”„๋ง, ํƒ€์ž„๋ฆฌํ”„์— ๊ตญ์ œํ™” ์ ์šฉํ•˜๊ธฐ

์Šคํ”„๋ง์„ ์‚ฌ์šฉํ•˜๋ฉด ๊ทธ๋ƒฅ message_en.properties๋ฅผ ์ถ”๊ฐ€๋กœ ์ž‘์„ฑํ•ด์ฃผ๋ฉด ์˜์–ด ๊ตญ์ œํ™”๊ฐ€ ์ ์šฉ๋œ๋‹ค. ๐Ÿ‘

 

  • ์ด๋Š” ์Šคํ”„๋ง์— LocaleResolver๋ผ๋Š” ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์กด์žฌํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
  • ์Šคํ”„๋ง ๋ถ€ํŠธ์—์„œ๋Š” LocaleResolver = new AcceptHeaderLocaleResolver() ๋ฅผ ๊ธฐ๋ณธ์œผ๋กœ ์‚ฌ์šฉํ•œ๋‹ค.
     ์ด๋Š” HTTP์˜ Accept-Language ํ—ค๋”๊ฐ’์„ ์ฝ์–ด์„œ Locale์„ ์„ค์ •ํ•˜๋Š” ๊ฐ์ฒด์ด๋‹ค.

์ฆ‰, ๋‹ค๋ฅธ๋ฐฉ์‹์œผ๋กœ ์‚ฌ์šฉ์ž์˜ ์–ธ์–ด๋ฅผ ํŒŒ์•…ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด LocaleResolver๋ฅผ ๋ณ€๊ฒฝํ•ด์ฃผ๋ฉด ๋œ๋‹ค. ์›น๋ธŒ๋ผ์šฐ์ €์˜ ํ—ค๋”๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ์‚ฌ์šฉ์ž์˜ Locale ์ •๋ณด๋ฅผ ์ฟ ํ‚ค, ์„ธ์…˜์— ๋ณด๊ด€ํ•ด์„œ ์‚ฌ์šฉํ•˜๊ธฐ๋„ ํ•œ๋‹ค.

๋ฌผ๋ก  ์ฟ ํ‚ค, ์„ธ์…˜์„ ์ด์šฉํ•˜๋Š” LocaleResolver ๊ตฌํ˜„์ฒด๋„ ์ด๋ฏธ ์Šคํ”„๋ง์—์„œ ์ œ๊ณตํ•ด์ฃผ๊ณ  ์žˆ๋‹ค.

์—ฌ๋‹ด์œผ๋กœ ์Šคํ”„๋ง์—์„œ๋Š” Internationalization ๋ฅผ [i ~18๊ธ€์ž ~n] ์ด๋ผ๋Š” ์˜๋ฏธ๋กœ i18n ํŒจํ‚ค์ง€๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

 

 

๋ธ”๋กœ๊ทธ์˜ ์ •๋ณด

JiwonDev

JiwonDev

ํ™œ๋™ํ•˜๊ธฐ