๐ŸŒ Spring Boot Internationalization: ๋‹ค๊ตญ์–ด ์ง€์›์˜ ๋งˆ๋ฒ• ๐Ÿช„

์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐ŸŒ Spring Boot Internationalization: ๋‹ค๊ตญ์–ด ์ง€์›์˜ ๋งˆ๋ฒ• ๐Ÿช„

 

 

์•ˆ๋…•ํ•˜์„ธ์š”, ์ฝ”๋”ฉ ๋งˆ๋ฒ•์‚ฌ ์—ฌ๋Ÿฌ๋ถ„! ์˜ค๋Š˜์€ ์•„์ฃผ ํŠน๋ณ„ํ•œ ์ฃผ๋ฌธ์„ ๋ฐฐ์›Œ๋ณผ ๊ฑฐ์˜ˆ์š”. ๋ฐ”๋กœ Spring Boot๋ฅผ ์‚ฌ์šฉํ•ด ์šฐ๋ฆฌ์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๋‹ค๊ตญ์–ด ์ง€์›์ด๋ผ๋Š” ๋งˆ๋ฒ•์„ ๊ฑธ์–ด๋ณด๋Š” ๊ฑฐ์ฃ ! ๐Ÿง™โ€โ™‚๏ธโœจ

์—ฌ๋Ÿฌ๋ถ„, ์ƒ์ƒํ•ด ๋ณด์„ธ์š”. ์—ฌ๋Ÿฌ๋ถ„์˜ ์›น์‚ฌ์ดํŠธ๊ฐ€ ์ „ ์„ธ๊ณ„ ์‚ฌ๋žŒ๋“ค๊ณผ ๋Œ€ํ™”๋ฅผ ๋‚˜๋ˆŒ ์ˆ˜ ์žˆ๋‹ค๋ฉด ์–ผ๋งˆ๋‚˜ ๋ฉ‹์งˆ๊นŒ์š”? ํ•œ๊ตญ์–ด, ์˜์–ด, ์ผ๋ณธ์–ด, ์ค‘๊ตญ์–ด... ๋งˆ์น˜ ๋ฐ”๋ฒจํƒ‘์˜ ์ฃผ๋ฌธ์„ ์™ธ์šด ๊ฒƒ์ฒ˜๋Ÿผ ๋ชจ๋“  ์–ธ์–ด๋กœ ์†Œํ†ตํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด ๋ง์ด์ฃ ! ๐Ÿ˜ฒ

์˜ค๋Š˜ ์šฐ๋ฆฌ๊ฐ€ ๋ฐฐ์šธ Spring Boot Internationalization์€ ๋ฐ”๋กœ ์ด๋Ÿฐ ๋งˆ๋ฒ•์„ ํ˜„์‹ค๋กœ ๋งŒ๋“ค์–ด์ฃผ๋Š” ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์ž…๋‹ˆ๋‹ค. ์ด ๊ธฐ์ˆ ์„ ์ตํžˆ๋ฉด, ์—ฌ๋Ÿฌ๋ถ„์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ๋งˆ์น˜ ์„ธ๊ณ„ ์—ฌํ–‰์„ ๋‹ค๋…€์˜จ ๊ฒƒ์ฒ˜๋Ÿผ ๋‹ค์–‘ํ•œ ์–ธ์–ด๋กœ ์‚ฌ์šฉ์ž๋“ค๊ณผ ์†Œํ†ตํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋  ๊ฑฐ์˜ˆ์š”!

๐ŸŒŸ ์žฌ๋Šฅ๋„ท ๊ฟ€ํŒ: ๋‹ค๊ตญ์–ด ์ง€์›์€ ๊ธ€๋กœ๋ฒŒ ์‹œ์žฅ์„ ๋…ธ๋ฆฌ๋Š” ์„œ๋น„์Šค์— ํ•„์ˆ˜์ ์ž…๋‹ˆ๋‹ค. ์žฌ๋Šฅ๋„ท์—์„œ๋„ ์ด ๊ธฐ์ˆ ์„ ํ™œ์šฉํ•ด ์ „ ์„ธ๊ณ„์˜ ์žฌ๋Šฅ ์žˆ๋Š” ๋ถ„๋“ค๊ณผ ์†Œํ†ตํ•˜๊ณ  ์žˆ๋‹ต๋‹ˆ๋‹ค! ์—ฌ๋Ÿฌ๋ถ„์˜ ์žฌ๋Šฅ์„ ์„ธ๊ณ„์™€ ๋‚˜๋ˆ„๊ณ  ์‹ถ๋‹ค๋ฉด, ์ด ๊ธฐ์ˆ ์„ ๊ผญ ์ตํ˜€๋‘์„ธ์š”!

์ž, ๊ทธ๋Ÿผ ์ด์ œ Spring Boot Internationalization์˜ ์„ธ๊ณ„๋กœ ๋น ์ ธ๋ณผ๊นŒ์š”? ์ค€๋น„๋˜์…จ๋‚˜์š”? ๋งˆ๋ฒ•์˜ ์ฃผ๋ฌธ์„ ์™ธ์น˜์„ธ์š”! "Internationalization Incantatum!" ๐Ÿช„โœจ

๐ŸŒˆ Spring Boot Internationalization์˜ ๊ธฐ์ดˆ ๋งˆ๋ฒ• ๐Ÿง™โ€โ™‚๏ธ

์šฐ๋ฆฌ์˜ ์ฒซ ๋ฒˆ์งธ ๋งˆ๋ฒ• ์ˆ˜์—…์„ ์‹œ์ž‘ํ•ด๋ณผ๊นŒ์š”? Spring Boot Internationalization, ์ค„์—ฌ์„œ i18n(internationalization์˜ ์ฒซ ๊ธ€์ž i์™€ ๋งˆ์ง€๋ง‰ ๊ธ€์ž n ์‚ฌ์ด์— 18๊ฐœ์˜ ๊ธ€์ž๊ฐ€ ์žˆ๋‹ค๋Š” ๋œป์ด์—์š”!)์ด๋ผ๊ณ  ๋ถ€๋ฅด๋Š” ์ด ๋งˆ๋ฒ•์€ ์šฐ๋ฆฌ์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์—ฌ๋Ÿฌ ์–ธ์–ด๋ฅผ ๋งํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๊ฐ•๋ ฅํ•œ ์ฃผ๋ฌธ์ด์—์š”.

๐Ÿ”ฎ i18n์˜ ๊ธฐ๋ณธ ์›๋ฆฌ

i18n์˜ ํ•ต์‹ฌ์€ ๋ฉ”์‹œ์ง€ ์†Œ์Šค(Message Source)๋ผ๋Š” ๋งˆ๋ฒ•์˜ ์ฑ…์— ์žˆ์–ด์š”. ์ด ์ฑ…์—๋Š” ์šฐ๋ฆฌ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋ชจ๋“  ํ…์ŠคํŠธ๊ฐ€ ์—ฌ๋Ÿฌ ์–ธ์–ด๋กœ ๋ฒˆ์—ญ๋˜์–ด ์žˆ์ฃ . ์‚ฌ์šฉ์ž์˜ ์–ธ์–ด ์„ค์ •์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ๋ฒˆ์—ญ์„ ๊ณจ๋ผ ๋ณด์—ฌ์ฃผ๋Š” ๊ฑฐ์˜ˆ์š”.

์˜ˆ๋ฅผ ๋“ค์–ด๋ณผ๊นŒ์š”?

  • ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด: "์•ˆ๋…•ํ•˜์„ธ์š”"
  • ๐Ÿ‡บ๐Ÿ‡ธ ์˜์–ด: "Hello"
  • ๐Ÿ‡ฏ๐Ÿ‡ต ์ผ๋ณธ์–ด: "ใ“ใ‚“ใซใกใฏ"
  • ๐Ÿ‡จ๐Ÿ‡ณ ์ค‘๊ตญ์–ด: "ไฝ ๅฅฝ"

์ด๋ ‡๊ฒŒ ๊ฐ ์–ธ์–ด๋ณ„๋กœ ๊ฐ™์€ ์˜๋ฏธ์˜ ๋ฌธ์žฅ์„ ์ค€๋น„ํ•ด๋‘๊ณ , ์‚ฌ์šฉ์ž์˜ ์„ค์ •์— ๋”ฐ๋ผ ์•Œ๋งž์€ ์–ธ์–ด๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๊ฑฐ์ฃ . ๋งˆ์น˜ ๋งˆ๋ฒ•์‚ฌ๊ฐ€ ์ƒํ™ฉ์— ๋งž๋Š” ์ฃผ๋ฌธ์„ ๊ณ ๋ฅด๋Š” ๊ฒƒ์ฒ˜๋Ÿผ์š”! ๐Ÿง™โ€โ™‚๏ธโœจ

๐Ÿ›  Spring Boot์—์„œ i18n ์„ค์ •ํ•˜๊ธฐ

์ž, ์ด์ œ ์šฐ๋ฆฌ์˜ Spring Boot ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์ด ๋งˆ๋ฒ•์„ ๊ฑธ์–ด๋ณผ๊นŒ์š”? ๋จผ์ € ํ•„์š”ํ•œ ๋งˆ๋ฒ• ๋„๊ตฌ๋“ค์„ ์ค€๋น„ํ•ด์•ผ ํ•ด์š”.

  1. ์˜์กด์„ฑ ์ถ”๊ฐ€: Spring Boot ์Šคํƒ€ํ„ฐ ์›น์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค๋ฉด, ์ด๋ฏธ ํ•„์š”ํ•œ ๋งˆ๋ฒ• ๋„๊ตฌ๋“ค์ด ํฌํ•จ๋˜์–ด ์žˆ์–ด์š”.
  2. ๋ฉ”์‹œ์ง€ ์†Œ์Šค ํŒŒ์ผ ์ƒ์„ฑ: src/main/resources ํด๋” ์•„๋ž˜์— messages.properties, messages_ko.properties, messages_en.properties ๋“ฑ์˜ ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด์š”.
  3. ์„ค์ • ํŒŒ์ผ ์ˆ˜์ •: application.properties ๋˜๋Š” application.yml ํŒŒ์ผ์— ๋ฉ”์‹œ์ง€ ์†Œ์Šค ์„ค์ •์„ ์ถ”๊ฐ€ํ•ด์š”.

์ด์ œ ๊ฐ๊ฐ์˜ ๋‹จ๊ณ„๋ฅผ ์ž์„ธํžˆ ์‚ดํŽด๋ณผ๊นŒ์š”?

1. ์˜์กด์„ฑ ์ถ”๊ฐ€

Spring Boot ์Šคํƒ€ํ„ฐ ์›น์„ ์‚ฌ์šฉ ์ค‘์ด๋ผ๋ฉด, ๋ณ„๋„์˜ ์˜์กด์„ฑ ์ถ”๊ฐ€๋Š” ํ•„์š” ์—†์–ด์š”. ํ•˜์ง€๋งŒ ํ™•์‹คํžˆ ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด, pom.xml ํŒŒ์ผ์— ๋‹ค์Œ ์˜์กด์„ฑ์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ด๋ณด์„ธ์š”:


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

2. ๋ฉ”์‹œ์ง€ ์†Œ์Šค ํŒŒ์ผ ์ƒ์„ฑ

src/main/resources ํด๋” ์•„๋ž˜์— ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํŒŒ์ผ๋“ค์„ ๋งŒ๋“ค์–ด์š”:

  • messages.properties (๊ธฐ๋ณธ ๋ฉ”์‹œ์ง€)
  • messages_ko.properties (ํ•œ๊ตญ์–ด ๋ฉ”์‹œ์ง€)
  • messages_en.properties (์˜์–ด ๋ฉ”์‹œ์ง€)
  • messages_ja.properties (์ผ๋ณธ์–ด ๋ฉ”์‹œ์ง€)
  • messages_zh.properties (์ค‘๊ตญ์–ด ๋ฉ”์‹œ์ง€)

๊ฐ ํŒŒ์ผ์—๋Š” ํ‚ค-๊ฐ’ ์Œ์œผ๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ์ •์˜ํ•ด์š”. ์˜ˆ๋ฅผ ๋“ค๋ฉด:


# messages.properties (๊ธฐ๋ณธ)
greeting=์•ˆ๋…•ํ•˜์„ธ์š”

# messages_en.properties
greeting=Hello

# messages_ja.properties
greeting=ใ“ใ‚“ใซใกใฏ

# messages_zh.properties
greeting=ไฝ ๅฅฝ

3. ์„ค์ • ํŒŒ์ผ ์ˆ˜์ •

application.properties ํŒŒ์ผ์— ๋‹ค์Œ ์„ค์ •์„ ์ถ”๊ฐ€ํ•ด์š”:


spring.messages.basename=messages
spring.messages.encoding=UTF-8

๋˜๋Š” application.yml ํŒŒ์ผ์„ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด:


spring:
  messages:
    basename: messages
    encoding: UTF-8

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด Spring Boot๊ฐ€ ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“  ๋ฉ”์‹œ์ง€ ํŒŒ์ผ๋“ค์„ ์ธ์‹ํ•˜๊ณ  ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ผ์š”.

๐ŸŒŸ ์žฌ๋Šฅ๋„ท ๊ฟ€ํŒ: ๋ฉ”์‹œ์ง€ ํŒŒ์ผ์„ ์ž‘์„ฑํ•  ๋•Œ๋Š” ์ผ๊ด€์„ฑ ์žˆ๋Š” ํ‚ค ๋„ค์ด๋ฐ ๊ทœ์น™์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์•„์š”. ์˜ˆ๋ฅผ ๋“ค์–ด, page.home.title, button.submit ๊ฐ™์€ ํ˜•์‹์œผ๋กœ ํ‚ค๋ฅผ ์ •์˜ํ•˜๋ฉด ๋‚˜์ค‘์— ๊ด€๋ฆฌํ•˜๊ธฐ ํ›จ์”ฌ ์‰ฌ์›Œ์ ธ์š”!

๐ŸŽญ Locale ๊ฒฐ์ •ํ•˜๊ธฐ

์ด์ œ ๋ฉ”์‹œ์ง€ ์†Œ์Šค๋ฅผ ์ค€๋น„ํ–ˆ์œผ๋‹ˆ, ์‚ฌ์šฉ์ž์˜ ์–ธ์–ด ์„ค์ •(Locale)์„ ์–ด๋–ป๊ฒŒ ๊ฒฐ์ •ํ• ์ง€ ์ •ํ•ด์•ผ ํ•ด์š”. Spring Boot๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ช‡ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•ด์š”:

  1. Accept-Language ํ—ค๋”: ๋ธŒ๋ผ์šฐ์ €์—์„œ ๋ณด๋‚ด๋Š” ์–ธ์–ด ์„ค์ •์„ ์‚ฌ์šฉํ•ด์š”.
  2. ์„ธ์…˜: ์‚ฌ์šฉ์ž ์„ธ์…˜์— ์ €์žฅ๋œ ์–ธ์–ด ์„ค์ •์„ ์‚ฌ์šฉํ•ด์š”.
  3. ์ฟ ํ‚ค: ์ฟ ํ‚ค์— ์ €์žฅ๋œ ์–ธ์–ด ์„ค์ •์„ ์‚ฌ์šฉํ•ด์š”.
  4. URL ํŒŒ๋ผ๋ฏธํ„ฐ: URL์— ์–ธ์–ด ์„ค์ •์„ ํฌํ•จ์‹œ์ผœ ์‚ฌ์šฉํ•ด์š”.

๊ธฐ๋ณธ์ ์œผ๋กœ Spring Boot๋Š” Accept-Language ํ—ค๋”๋ฅผ ์‚ฌ์šฉํ•˜์ง€๋งŒ, ์šฐ๋ฆฌ๋Š” ์ด๋ฅผ ์ปค์Šคํ„ฐ๋งˆ์ด์ฆˆํ•  ์ˆ˜ ์žˆ์–ด์š”. ์˜ˆ๋ฅผ ๋“ค์–ด, URL ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์–ธ์–ด๋ฅผ ์„ ํƒํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์„ค์ •์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด์š”:


@Configuration
public class LocaleConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        localeChangeInterceptor.setParamName("lang");
        registry.addInterceptor(localeChangeInterceptor);
    }

    @Bean
    public LocaleResolver localeResolver() {
        SessionLocaleResolver slr = new SessionLocaleResolver();
        slr.setDefaultLocale(Locale.KOREAN);
        return slr;
    }
}

์ด ์„ค์ •์„ ์‚ฌ์šฉํ•˜๋ฉด, URL์— ?lang=en๊ณผ ๊ฐ™์€ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•ด ์–ธ์–ด๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์–ด์š”.

๐Ÿงช ๋ฉ”์‹œ์ง€ ์‚ฌ์šฉํ•˜๊ธฐ

์ด์ œ ๋ชจ๋“  ์ค€๋น„๊ฐ€ ๋๋‚ฌ์–ด์š”! ์šฐ๋ฆฌ์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ๋‹ค๊ตญ์–ด ๋ฉ”์‹œ์ง€๋ฅผ ์‚ฌ์šฉํ•ด๋ณผ๊นŒ์š”?

์ปจํŠธ๋กค๋Ÿฌ์—์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ฉ”์‹œ์ง€๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์–ด์š”:


@Controller
public class HomeController {
    @Autowired
    private MessageSource messageSource;

    @GetMapping("/")
    public String home(Model model, Locale locale) {
        String greeting = messageSource.getMessage("greeting", null, locale);
        model.addAttribute("greeting", greeting);
        return "home";
    }
}

๊ทธ๋ฆฌ๊ณ  Thymeleaf ํ…œํ”Œ๋ฆฟ์—์„œ๋Š” ์ด๋ ‡๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”:


<h1 th:text="#{greeting}"></h1>

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด, ์‚ฌ์šฉ์ž์˜ ์–ธ์–ด ์„ค์ •์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ์ธ์‚ฌ๋ง์ด ํ‘œ์‹œ๋  ๊ฑฐ์˜ˆ์š”!

๐ŸŒŸ ์žฌ๋Šฅ๋„ท ๊ฟ€ํŒ: ๋‹ค๊ตญ์–ด ์ง€์›์€ ๋‹จ์ˆœํžˆ ํ…์ŠคํŠธ๋ฅผ ๋ฒˆ์—ญํ•˜๋Š” ๊ฒƒ ์ด์ƒ์ด์—์š”. ๋‚ ์งœ ํ˜•์‹, ์ˆซ์ž ํ˜•์‹, ํ†ตํ™” ๋“ฑ๋„ ์ง€์—ญ์— ๋”ฐ๋ผ ๋‹ค๋ฅด๊ฒŒ ํ‘œ์‹œํ•ด์•ผ ํ•ด์š”. Spring์˜ MessageSource์™€ ํ•จ๊ป˜ java.text.NumberFormat, java.text.DateFormat ๋“ฑ์„ ํ™œ์šฉํ•˜๋ฉด ๋”์šฑ ์™„๋ฒฝํ•œ ํ˜„์ง€ํ™”๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค!

์—ฌ๊ธฐ๊นŒ์ง€ Spring Boot Internationalization์˜ ๊ธฐ์ดˆ ๋งˆ๋ฒ•์„ ๋ฐฐ์›Œ๋ดค์–ด์š”. ์ด์ œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ์„ธ๊ณ„ ๊ฐ๊ตญ์˜ ์–ธ์–ด๋กœ ์‚ฌ์šฉ์ž๋“ค๊ณผ ๋Œ€ํ™”ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์–ด์š”! ๐ŸŒโœจ

๋‹ค์Œ ์„น์…˜์—์„œ๋Š” ๋” ๊ณ ๊ธ‰ ๋งˆ๋ฒ•์„ ๋ฐฐ์›Œ๋ณผ ๊ฑฐ์˜ˆ์š”. ์ค€๋น„๋˜์…จ๋‚˜์š”? ์šฐ๋ฆฌ์˜ ๋งˆ๋ฒ• ์—ฌํ–‰์€ ๊ณ„์†๋ฉ๋‹ˆ๋‹ค! ๐Ÿง™โ€โ™‚๏ธ๐Ÿš€

๐Ÿš€ Spring Boot Internationalization์˜ ๊ณ ๊ธ‰ ๋งˆ๋ฒ• ๐Ÿง™โ€โ™€๏ธ

์ž, ์ด์ œ ์šฐ๋ฆฌ๋Š” ๊ธฐ๋ณธ์ ์ธ ๋‹ค๊ตญ์–ด ์ง€์› ๋งˆ๋ฒ•์„ ์ตํ˜”์–ด์š”. ํ•˜์ง€๋งŒ ์ง„์ •ํ•œ ๋งˆ๋ฒ•์‚ฌ๊ฐ€ ๋˜๋ ค๋ฉด ๋” ๊นŠ์ด ์žˆ๋Š” ์ง€์‹์ด ํ•„์š”ํ•˜์ฃ . ์ด๋ฒˆ์—๋Š” Spring Boot Internationalization์˜ ๊ณ ๊ธ‰ ๋งˆ๋ฒ•์„ ๋ฐฐ์›Œ๋ณผ ๊ฑฐ์˜ˆ์š”. ์ค€๋น„๋˜์…จ๋‚˜์š”? ๋งˆ๋ฒ• ์ง€ํŒก์ด๋ฅผ ๊ผญ ์ฅ๊ณ , ์ฃผ๋ฌธ์„ ์™ธ์น  ์ค€๋น„๋ฅผ ํ•˜์„ธ์š”! ๐Ÿช„โœจ

๐ŸŒŸ ๋™์  ๋ฉ”์‹œ์ง€์™€ ํŒŒ๋ผ๋ฏธํ„ฐ ์‚ฌ์šฉํ•˜๊ธฐ

๋•Œ๋กœ๋Š” ๋ฉ”์‹œ์ง€์— ๋™์ ์ธ ๊ฐ’์„ ๋„ฃ์–ด์•ผ ํ•  ๋•Œ๊ฐ€ ์žˆ์–ด์š”. ์˜ˆ๋ฅผ ๋“ค์–ด, ์‚ฌ์šฉ์ž์˜ ์ด๋ฆ„์„ ๋„ฃ์–ด ์ธ์‚ฌํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ• ๊นŒ์š”?

๋ฉ”์‹œ์ง€ ํŒŒ์ผ์— ์ด๋ ‡๊ฒŒ ์ •์˜ํ•  ์ˆ˜ ์žˆ์–ด์š”:


# messages.properties
greeting.name=์•ˆ๋…•ํ•˜์„ธ์š”, {0}๋‹˜!

# messages_en.properties
greeting.name=Hello, {0}!

๊ทธ๋ฆฌ๊ณ  ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์ด๋ ‡๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”:


@GetMapping("/greet")
public String greet(@RequestParam String name, Model model, Locale locale) {
    String greeting = messageSource.getMessage("greeting.name", new Object[]{name}, locale);
    model.addAttribute("greeting", greeting);
    return "greet";
}

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด, /greet?name=Alice๋กœ ์ ‘์†ํ–ˆ์„ ๋•Œ "์•ˆ๋…•ํ•˜์„ธ์š”, Alice๋‹˜!" ๋˜๋Š” "Hello, Alice!"๋ผ๊ณ  ํ‘œ์‹œ๋  ๊ฑฐ์˜ˆ์š”.

โš ๏ธ ์ฃผ์˜: ๋™์  ๋ฉ”์‹œ์ง€๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ์‚ฌ์šฉ์ž ์ž…๋ ฅ์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋„๋ก ์ฃผ์˜ํ•ด์•ผ ํ•ด์š”. XSS(Cross-Site Scripting) ๊ณต๊ฒฉ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ํ•ญ์ƒ ์‚ฌ์šฉ์ž ์ž…๋ ฅ์„ ๊ฒ€์ฆํ•˜๊ณ  ์ด์Šค์ผ€์ดํ”„ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค!

๐ŸŽญ ๋ณต์ˆ˜ํ˜• ์ฒ˜๋ฆฌํ•˜๊ธฐ

์–ธ์–ด๋งˆ๋‹ค ๋ณต์ˆ˜ํ˜•์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์ด ๋‹ค๋ฅด์ฃ . Spring์˜ ๋ฉ”์‹œ์ง€ ์†Œ์Šค๋Š” ์ด๋Ÿฐ ๋ณต์žกํ•œ ๋ณต์ˆ˜ํ˜• ์ฒ˜๋ฆฌ๋„ ์ง€์›ํ•ด์š”.

๋ฉ”์‹œ์ง€ ํŒŒ์ผ์— ์ด๋ ‡๊ฒŒ ์ •์˜ํ•  ์ˆ˜ ์žˆ์–ด์š”:


# messages.properties
items.count={0}๊ฐœ์˜ ์•„์ดํ…œ์ด ์žˆ์Šต๋‹ˆ๋‹ค.
items.count.zero=์•„์ดํ…œ์ด ์—†์Šต๋‹ˆ๋‹ค.
items.count.one=1๊ฐœ์˜ ์•„์ดํ…œ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

# messages_en.properties
items.count={0} items
items.count.zero=No items
items.count.one=One item

๊ทธ๋ฆฌ๊ณ  ์ฝ”๋“œ์—์„œ ์ด๋ ‡๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”:


@GetMapping("/items")
public String items(@RequestParam int count, Model model, Locale locale) {
    String message = messageSource.getMessage("items.count", new Object[]{count}, locale);
    model.addAttribute("message", message);
    return "items";
}

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์•„์ดํ…œ์˜ ๊ฐœ์ˆ˜์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ๋ฉ”์‹œ์ง€๊ฐ€ ํ‘œ์‹œ๋  ๊ฑฐ์˜ˆ์š”.

๐ŸŒˆ ๋ฆฌ์†Œ์Šค ๋ฒˆ๋“ค ๊ณ„์ธต ๊ตฌ์กฐ

๋ฉ”์‹œ์ง€ ๋ฆฌ์†Œ์Šค๋Š” ๊ณ„์ธต ๊ตฌ์กฐ๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ์–ด์š”. ์˜ˆ๋ฅผ ๋“ค์–ด, ๊ณตํ†ต ๋ฉ”์‹œ์ง€๋Š” ๊ธฐ๋ณธ ํŒŒ์ผ์— ๋‘๊ณ , ํŠน์ • ์–ธ์–ด์—๋งŒ ํ•ด๋‹นํ•˜๋Š” ๋ฉ”์‹œ์ง€๋Š” ํ•ด๋‹น ์–ธ์–ด ํŒŒ์ผ์— ๋‘˜ ์ˆ˜ ์žˆ์ฃ .


# messages.properties (๊ธฐ๋ณธ)
common.submit=์ œ์ถœ
common.cancel=์ทจ์†Œ

# messages_en.properties
common.submit=Submit
common.cancel=Cancel

# messages_ko_KR.properties
specific.korean.message=ํ•œ๊ตญ์–ด ํŠนํ™” ๋ฉ”์‹œ์ง€

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ํ•œ๊ตญ์–ด ์‚ฌ์šฉ์ž๋Š” specific.korean.message๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์ง€๋งŒ, ๋‹ค๋ฅธ ์–ธ์–ด ์‚ฌ์šฉ์ž๋Š” ์ด ๋ฉ”์‹œ์ง€๋ฅผ ๋ณผ ์ˆ˜ ์—†์–ด์š”. ๋Œ€์‹  ๊ณตํ†ต ๋ฉ”์‹œ์ง€์ธ common.submit๊ณผ common.cancel์€ ๋ชจ๋“  ์–ธ์–ด์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์ฃ .

๐Ÿ”„ ๋Ÿฐํƒ€์ž„์— ๋ฉ”์‹œ์ง€ ๋ณ€๊ฒฝํ•˜๊ธฐ

๋•Œ๋กœ๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์žฌ์‹œ์ž‘ํ•˜์ง€ ์•Š๊ณ ๋„ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณ€๊ฒฝํ•ด์•ผ ํ•  ๋•Œ๊ฐ€ ์žˆ์–ด์š”. ์ด๋Ÿด ๋•Œ๋Š” ReloadableResourceBundleMessageSource๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”.


@Bean
public MessageSource messageSource() {
    ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
    messageSource.setBasename("classpath:messages");
    messageSource.setDefaultEncoding("UTF-8");
    messageSource.setCacheSeconds(10);  // 10์ดˆ๋งˆ๋‹ค ๋ฆฌ๋กœ๋“œ
    return messageSource;
}

์ด๋ ‡๊ฒŒ ์„ค์ •ํ•˜๋ฉด, ๋ฉ”์‹œ์ง€ ํŒŒ์ผ์„ ์ˆ˜์ •ํ•˜๊ณ  10์ดˆ๋งŒ ๊ธฐ๋‹ค๋ฆฌ๋ฉด ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์ ์šฉ๋ผ์š”. ๋งˆ์น˜ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ฃผ๋ฌธ์„ ๋ฐ”๊พธ๋Š” ๊ฒƒ ๊ฐ™์ฃ ? ๐Ÿง™โ€โ™‚๏ธโœจ

๐ŸŒ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์‚ฌ์šฉํ•œ ๋ฉ”์‹œ์ง€ ๊ด€๋ฆฌ

๋ฉ”์‹œ์ง€๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์•„์ง€๋ฉด ํŒŒ์ผ๋กœ ๊ด€๋ฆฌํ•˜๊ธฐ ์–ด๋ ค์šธ ์ˆ˜ ์žˆ์–ด์š”. ์ด๋Ÿด ๋•Œ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์‚ฌ์šฉํ•ด ๋ฉ”์‹œ์ง€๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์š”.

๋จผ์ €, ๋ฉ”์‹œ์ง€๋ฅผ ์ €์žฅํ•  ํ…Œ์ด๋ธ”์„ ๋งŒ๋“ค์–ด์š”:


CREATE TABLE messages (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    locale VARCHAR(10),
    code VARCHAR(255),
    message TEXT
);

๊ทธ๋ฆฌ๊ณ  ์ด ํ…Œ์ด๋ธ”์—์„œ ๋ฉ”์‹œ์ง€๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ์ปค์Šคํ…€ MessageSource๋ฅผ ๋งŒ๋“ค์–ด์š”:


@Component
public class DatabaseMessageSource extends AbstractMessageSource {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    protected MessageFormat resolveCode(String code, Locale locale) {
        String message = jdbcTemplate.queryForObject(
            "SELECT message FROM messages WHERE code = ? AND locale = ?",
            new Object[]{code, locale.toString()},
            String.class
        );
        return new MessageFormat(message, locale);
    }
}

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๋ฉ”์‹œ์ง€๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์š”. ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค์–ด ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ์ˆ˜์ •ํ•  ์ˆ˜๋„ ์žˆ์ฃ !

๐ŸŒŸ ์žฌ๋Šฅ๋„ท ๊ฟ€ํŒ: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์‚ฌ์šฉํ•ด ๋ฉ”์‹œ์ง€๋ฅผ ๊ด€๋ฆฌํ•˜๋ฉด, ๋ฒˆ์—ญ๊ฐ€๋“ค์ด ์ง์ ‘ ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€์—์„œ ๋ฒˆ์—ญ์„ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ์–ด์š”. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๊ฐœ๋ฐœ์ž๊ฐ€ ๋งค๋ฒˆ ํŒŒ์ผ์„ ์ˆ˜์ •ํ•˜๊ณ  ๋ฐฐํฌํ•  ํ•„์š” ์—†์ด ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ฒˆ์—ญ์„ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค!

๐ŸŽจ ๋‹ค๊ตญ์–ด ์ง€์›์„ ์œ„ํ•œ UI/UX ๊ณ ๋ ค์‚ฌํ•ญ

๋‹ค๊ตญ์–ด ์ง€์›์€ ๋‹จ์ˆœํžˆ ํ…์ŠคํŠธ๋ฅผ ๋ฒˆ์—ญํ•˜๋Š” ๊ฒƒ ์ด์ƒ์ด์—์š”. UI/UX ์ธก๋ฉด์—์„œ๋„ ๋ช‡ ๊ฐ€์ง€ ๊ณ ๋ คํ•ด์•ผ ํ•  ์ ์ด ์žˆ์ฃ .

  1. ์–ธ์–ด ์„ ํƒ UI: ์‚ฌ์šฉ์ž๊ฐ€ ์‰ฝ๊ฒŒ ์–ธ์–ด๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋„๋ก ์–ธ์–ด ์„ ํƒ ๋“œ๋กญ๋‹ค์šด์ด๋‚˜ ํ”Œ๋ž˜๊ทธ ์•„์ด์ฝ˜์„ ์ œ๊ณตํ•˜์„ธ์š”.
  2. ํ…์ŠคํŠธ ๊ธธ์ด ๊ณ ๋ ค: ๋ฒˆ์—ญ๋œ ํ…์ŠคํŠธ์˜ ๊ธธ์ด๊ฐ€ ์›๋ณธ๊ณผ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์–ด์š”. UI๊ฐ€ ๊นจ์ง€์ง€ ์•Š๋„๋ก ์œ ์—ฐํ•œ ๋ ˆ์ด์•„์›ƒ์„ ์‚ฌ์šฉํ•˜์„ธ์š”.
  3. ๋‚ ์งœ์™€ ์‹œ๊ฐ„ ํ˜•์‹: ๋‚ ์งœ์™€ ์‹œ๊ฐ„ ํ‘œ์‹œ ํ˜•์‹๋„ ์ง€์—ญ์— ๋”ฐ๋ผ ๋‹ค๋ฆ…๋‹ˆ๋‹ค. Java์˜ DateTimeFormatter๋ฅผ ํ™œ์šฉํ•˜์„ธ์š”.
  4. ์ˆซ์ž์™€ ํ†ตํ™” ํ˜•์‹: ์ˆซ์ž์™€ ํ†ตํ™” ํ‘œ์‹œ ๋ฐฉ์‹๋„ ์ง€์—ญ๋งˆ๋‹ค ๋‹ค๋ฆ…๋‹ˆ๋‹ค. NumberFormat์„ ์‚ฌ์šฉํ•ด ์ ์ ˆํžˆ ํฌ๋งทํŒ…ํ•˜์„ธ์š”.
  5. ์ด๋ฏธ์ง€์™€ ์•„์ด์ฝ˜: ๋ฌธํ™”์  ์ฐจ์ด๋ฅผ ๊ณ ๋ คํ•ด ์ ์ ˆํ•œ ์ด๋ฏธ์ง€์™€ ์•„์ด์ฝ˜์„ ์‚ฌ์šฉํ•˜์„ธ์š”.

๐Ÿงช ๋‹ค๊ตญ์–ด ์ง€์› ํ…Œ์ŠคํŠธํ•˜๊ธฐ

๋‹ค๊ตญ์–ด ์ง€์›์ด ์ œ๋Œ€๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ๋„ ์ค‘์š”ํ•ด์š”. Spring Boot์—์„œ๋Š” ์ด๋ ‡๊ฒŒ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์–ด์š”:


@SpringBootTest
@AutoConfigureMockMvc
public class InternationalizationTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testGreetingInKorean() throws Exception {
        mockMvc.perform(get("/greet?name=Alice")
                .header("Accept-Language", "ko-KR"))
                .andExpect(status().isOk())
                .andExpect(content().string(containsString("์•ˆ๋…•ํ•˜์„ธ์š”, Alice๋‹˜!")));
    }

    @Test
    public void testGreetingInEnglish() throws Exception {
        mockMvc.perform(get("/greet?name=Alice")
                .header("Accept-Language", "en-US"))
                .andExpect(status().isOk())
                .andExpect(content().string(containsString("Hello, Alice!")));
    }
}

์ด๋Ÿฐ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ด ๊ฐ ์–ธ์–ด๋ณ„๋กœ ๋ฉ”์‹œ์ง€๊ฐ€ ์ œ๋Œ€๋กœ ํ‘œ์‹œ๋˜๋Š”์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด์š”.

๐ŸŒ ๊ตญ์ œํ™”์™€ ์ง€์—ญํ™”์˜ ์ฐจ์ด

๋งˆ์ง€๋ง‰์œผ๋กœ, ๊ตญ์ œํ™”(Internationalization, i18n)์™€ ์ง€์—ญํ™”(Localization, l10n)์˜ ์ฐจ์ด์— ๋Œ€ํ•ด ์•Œ์•„๋ณผ๊นŒ์š”?

  • ๊ตญ์ œํ™”(i18n): ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋‹ค์–‘ํ•œ ์–ธ์–ด์™€ ์ง€์—ญ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค๊ณ„ํ•˜๊ณ  ๊ฐœ๋ฐœํ•˜๋Š” ๊ณผ์ •์ด์—์š”. ์šฐ๋ฆฌ๊ฐ€ ์ง€๊ธˆ๊นŒ์ง€ ๋ฐฐ์šด Spring Boot์˜ ๋‹ค๊ตญ์–ด ์ง€์› ๊ธฐ๋Šฅ์ด ๋ฐ”๋กœ ์ด ๊ตญ์ œํ™”์— ํ•ด๋‹นํ•ด์š”.
  • ์ง€์—ญํ™”(l10n): ๊ตญ์ œํ™”๋œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ํŠน์ • ์–ธ์–ด๋‚˜ ์ง€์—ญ์— ๋งž๊ฒŒ ์กฐ์ •ํ•˜๋Š” ๊ณผ์ •์ด์—์š”. ํ…์ŠคํŠธ ๋ฒˆ์—ญ, ๋‚ ์งœ/์‹œ๊ฐ„ ํ˜•์‹ ์กฐ์ •, ํ†ตํ™” ๋‹จ์œ„ ๋ณ€๊ฒฝ ๋“ฑ์ด ์—ฌ๊ธฐ์— ํฌํ•จ๋ผ์š”.

์ฆ‰, ๊ตญ์ œํ™”๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์—ฌ๋Ÿฌ ์–ธ์–ด๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“œ๋Š” ๊ธฐ์ˆ ์ ์ธ ๊ณผ์ •์ด๊ณ , ์ง€์—ญํ™”๋Š” ์‹ค์ œ๋กœ ๊ฐ ์–ธ์–ด์™€ ๋ฌธํ™”์— ๋งž๊ฒŒ ์ฝ˜ํ…์ธ ๋ฅผ ์กฐ์ •ํ•˜๋Š” ๊ณผ์ •์ด๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ์–ด์š”.

๐ŸŒŸ ์žฌ๋Šฅ๋„ท ๊ฟ€ํŒ: ์™„๋ฒฝํ•œ ๋‹ค๊ตญ์–ด ์ง€์›์„ ์œ„ํ•ด์„œ๋Š” ๊ตญ์ œํ™”์™€ ์ง€์—ญํ™” ๋ชจ๋‘๊ฐ€ ์ค‘์š”ํ•ด์š”. ๊ธฐ์ˆ ์ ์œผ๋กœ ๋‹ค๊ตญ์–ด๋ฅผ ์ง€์›ํ•˜๋Š” ๊ฒƒ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ, ๊ฐ ์ง€์—ญ์˜ ๋ฌธํ™”์™€ ๊ด€์Šต์„ ์ดํ•ดํ•˜๊ณ  ๊ทธ์— ๋งž๊ฒŒ ์ฝ˜ํ…์ธ ๋ฅผ ์กฐ์ •ํ•˜๋Š” ๊ฒƒ๋„ ์žŠ์ง€ ๋งˆ์„ธ์š”!

์—ฌ๊ธฐ๊นŒ์ง€ Spring Boot Internationalization์˜ ๊ณ ๊ธ‰ ๋งˆ๋ฒ•์„ ๋ฐฐ์›Œ๋ดค์–ด์š”. ์ด์ œ ์—ฌ๋Ÿฌ๋ถ„์€ ์ง„์ •ํ•œ ๊ตญ์ œํ™” ๋งˆ๋ฒ•์‚ฌ๊ฐ€ ๋˜์—ˆ์–ด์š”! ๐Ÿง™โ€โ™‚๏ธโœจ ์ด ๋งˆ๋ฒ•์„ ํ™œ์šฉํ•ด ์—ฌ๋Ÿฌ๋ถ„์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์ „ ์„ธ๊ณ„ ์‚ฌ์šฉ์ž๋“ค๊ณผ ์†Œํ†ตํ•  ์ˆ˜ ์žˆ๊ธฐ๋ฅผ ๋ฐ”๋ผ์š”. ๋‹ค์Œ ์„น์…˜์—์„œ๋Š” ์‹ค์ œ ํ”„๋กœ์ ํŠธ์— ์ด ๋งˆ๋ฒ•์„ ์ ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ž์„ธํžˆ ์•Œ์•„๋ณผ ๊ฑฐ์˜ˆ์š”. ์ค€๋น„๋˜์…จ๋‚˜์š”? ์šฐ๋ฆฌ์˜ ๋งˆ๋ฒ• ์—ฌํ–‰์€ ๊ณ„์†๋ฉ๋‹ˆ๋‹ค! ๐Ÿš€๐ŸŒ

๐Ÿ—๏ธ Spring Boot Internationalization ์‹ค์ „ ํ”„๋กœ์ ํŠธ ์ ์šฉํ•˜๊ธฐ ๐Ÿ› ๏ธ

์ž, ์ด์ œ ์šฐ๋ฆฌ๋Š” Spring Boot Internationalization์˜ ๊ธฐ๋ณธ๋ถ€ํ„ฐ ๊ณ ๊ธ‰ ๋งˆ๋ฒ•๊นŒ์ง€ ๋ชจ๋‘ ๋ฐฐ์› ์–ด์š”. ์ด์ œ ์ด ์ง€์‹์„ ์‹ค์ œ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•ด๋ณผ ์‹œ๊ฐ„์ด์—์š”! ๋งˆ๋ฒ• ์ง€ํŒก์ด๋ฅผ ๊ผญ ์ฅ๊ณ , ์šฐ๋ฆฌ์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์„ธ๊ณ„์ ์ธ ์„œ๋น„์Šค๋กœ ๋งŒ๋“ค์–ด๋ณผ๊นŒ์š”? ๐ŸŒโœจ

๐ŸŽจ ๋‹ค๊ตญ์–ด ์ง€์› ์‡ผํ•‘๋ชฐ ํ”„๋กœ์ ํŠธ ์‹œ์ž‘ํ•˜๊ธฐ

์šฐ๋ฆฌ๋Š” "์žฌ๋Šฅ๋„ท ๊ธ€๋กœ๋ฒŒ ๋งˆ์ผ“"์ด๋ผ๋Š” ์ด๋ฆ„์˜ ๋‹ค๊ตญ์–ด ์ง€์› ์‡ผํ•‘๋ชฐ์„ ๋งŒ๋“ค์–ด๋ณผ ๊ฑฐ์˜ˆ์š”. ์ด ์‡ผํ•‘๋ชฐ์€ ์ „ ์„ธ๊ณ„์˜ ๋‹ค์–‘ํ•œ ์žฌ๋Šฅ์„ ์‚ฌ๊ณ ํŒ” ์ˆ˜ ์žˆ๋Š” ํ”Œ๋žซํผ์ด ๋  ๊ฑฐ์˜ˆ์š”. ์–ด๋–ค ๊ธฐ๋Šฅ๋“ค์ด ํ•„์š”ํ• ๊นŒ์š”?

  1. ๋‹ค๊ตญ์–ด ์ง€์› (ํ•œ๊ตญ์–ด, ์˜์–ด, ์ผ๋ณธ์–ด, ์ค‘๊ตญ์–ด)
  2. ์ƒํ’ˆ ๋ชฉ๋ก ๋ฐ ์ƒ์„ธ ํŽ˜์ด์ง€
  3. ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๊ธฐ๋Šฅ
  4. ๊ฒฐ์ œ ์‹œ์Šคํ…œ
  5. ์‚ฌ์šฉ์ž ํ”„๋กœํ•„

์ด ๊ธฐ๋Šฅ๋“ค์„ ๊ตฌํ˜„ํ•˜๋ฉด์„œ ๋‹ค๊ตญ์–ด ์ง€์›์„ ์–ด๋–ป๊ฒŒ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ์‚ดํŽด๋ณผ๊ฒŒ์š”.

๐Ÿ› ๏ธ ํ”„๋กœ์ ํŠธ ์„ค์ •

๋จผ์ € Spring Boot ํ”„๋กœ์ ํŠธ๋ฅผ ์„ค์ •ํ•ด๋ณผ๊นŒ์š”?


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

๊ทธ๋ฆฌ๊ณ  application.properties ํŒŒ์ผ์— ๋‹ค์Œ ์„ค์ •์„ ์ถ”๊ฐ€ํ•ด์š”:


spring.messages.basename=i18n/messages
spring.messages.encoding=UTF-8

๐ŸŒ ๋ฉ”์‹œ์ง€ ๋ฆฌ์†Œ์Šค ํŒŒ์ผ ๋งŒ๋“ค๊ธฐ

src/main/resources/i18n ํด๋”๋ฅผ ๋งŒ๋“ค๊ณ , ๊ทธ ์•ˆ์— ๋‹ค์Œ ํŒŒ์ผ๋“ค์„ ๋งŒ๋“ค์–ด์š”:

  • messages.properties (๊ธฐ๋ณธ ๋ฉ”์‹œ์ง€)
  • messages_ko.properties (ํ•œ๊ตญ์–ด)
  • messages_en.properties (์˜์–ด)
  • messages_ja.properties (์ผ๋ณธ์–ด)
  • messages_zh.properties (์ค‘๊ตญ์–ด)

๊ฐ ํŒŒ์ผ์— ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ฉ”์‹œ์ง€๋ฅผ ์ถ”๊ฐ€ํ•ด๋ณผ๊นŒ์š”?


# messages.properties
home.welcome=์žฌ๋Šฅ๋„ท ๊ธ€๋กœ๋ฒŒ ๋งˆ์ผ“์— ์˜ค์‹  ๊ฒƒ์„ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค!
nav.home=ํ™ˆ
nav.products=์ƒํ’ˆ
nav.cart=์žฅ๋ฐ”๊ตฌ๋‹ˆ
nav.profile=ํ”„๋กœํ•„

# messages_en.properties
home.welcome=Welcome to TalentNet Global Market!
nav.home=Home
nav.products=Products
nav.cart=Cart
nav.profile=Profile

# (๋‹ค๋ฅธ ์–ธ์–ด ํŒŒ์ผ๋„ ๋น„์Šทํ•˜๊ฒŒ ์ž‘์„ฑ)

๐Ÿ  ํ™ˆ ์ปจํŠธ๋กค๋Ÿฌ ๋งŒ๋“ค๊ธฐ

์ด์ œ ํ™ˆ ํŽ˜์ด์ง€๋ฅผ ์œ„ํ•œ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”:


@Controller
public class HomeController {

    @GetMapping("/")
    public String home() {
        return "home";
    }
}

๐ŸŽจ Thymeleaf ํ…œํ”Œ๋ฆฟ ๋งŒ๋“ค๊ธฐ

src/main/resources/templates/home.html ํŒŒ์ผ์„ ๋งŒ๋“ค๊ณ  ๋‹ค์Œ ๋‚ด์šฉ์„ ์ถ”๊ฐ€ํ•ด์š”:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>์žฌ๋Šฅ๋„ท ๊ธ€๋กœ๋ฒŒ ๋งˆ์ผ“</title>
</head>
<body>
    <h1 th:text="#{home.welcome}">ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค!</h1>
    <nav>
        <ul>
            <li><a href="#" th:text="#{nav.home}">ํ™ˆ</a></li>
            <li><a href="#" th:text="#{nav.products}">์ƒํ’ˆ</a></li>
            <li><a href="#" th:text="#{nav.cart}">์žฅ๋ฐ”๊ตฌ๋‹ˆ</a></li>
            <li><a href="#" th:text="#{nav.profile}">ํ”„๋กœํ•„</a></li>
        </ul>
    </nav>
</body>
</html>

๐ŸŒ ์–ธ์–ด ๋ณ€๊ฒฝ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ํ•˜๊ธฐ

์‚ฌ์šฉ์ž๊ฐ€ ์–ธ์–ด๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ด๋ณผ๊นŒ์š”?

๋จผ์ €, LocaleChangeInterceptor๋ฅผ ์„ค์ •ํ•ด์š”:


@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
        lci.setParamName("lang");
        return lci;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }

    @Bean
    public LocaleResolver localeResolver() {
        SessionLocaleResolver slr = new SessionLocaleResolver();
        slr.setDefaultLocale(Locale.KOREAN);
        return slr;
    }
}

๊ทธ๋ฆฌ๊ณ  ํ™ˆ ํŽ˜์ด์ง€์— ์–ธ์–ด ์„ ํƒ ๋งํฌ๋ฅผ ์ถ”๊ฐ€ํ•ด์š”:


<div>
    <a href="?lang=ko">ํ•œ๊ตญ์–ด</a>
    <a href="?lang=en">English</a>
    <a href="?lang=ja">ๆ—ฅๆœฌ่ชž</a>
    <a href="?lang=zh">ไธญๆ–‡</a>
</div>

๐Ÿ›๏ธ ์ƒํ’ˆ ๋ชฉ๋ก ํŽ˜์ด์ง€ ๋งŒ๋“ค๊ธฐ

์ด์ œ ์ƒํ’ˆ ๋ชฉ๋ก ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊นŒ์š”? ๋จผ์ € ์ƒํ’ˆ ๋ชจ๋ธ์„ ๋งŒ๋“ค์–ด์š”:


public class Product {
    private Long id;
    private String nameKey;
    private String descriptionKey;
    private BigDecimal price;

    // ์ƒ์„ฑ์ž, getter, setter ์ƒ๋žต
}

์ƒํ’ˆ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ๋งŒ๋“ค์–ด์š”:


@Controller
@RequestMapping("/products")
public class ProductController {

    @Autowired
    private MessageSource messageSource;

    @GetMapping
    public String listProducts(Model model) {
        List<Product> products = Arrays.asList(
            new Product(1L, "product.name.1", "product.desc.1", new BigDecimal("100.00")),
            new Product(2L, "product.name.2", "product.desc.2", new BigDecimal("200.00"))
        );
        model.addAttribute("products", products);
        return "products";
    }
}

๊ทธ๋ฆฌ๊ณ  products.html ํ…œํ”Œ๋ฆฟ์„ ๋งŒ๋“ค์–ด์š”:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>์ƒํ’ˆ ๋ชฉ๋ก</title>
</head>
<body>
    <h1 th:text="#{products.title}">์ƒํ’ˆ ๋ชฉ๋ก</h1>
    <ul>
        <li th:each="product : ${products}">
            <span th:text="#{${product.nameKey}}">์ƒํ’ˆ ์ด๋ฆ„</span>
            <p th:text="#{${product.descriptionKey}}">์ƒํ’ˆ ์„ค๋ช…</p>
            <span th:text="${#numbers.formatCurrency(product.price)}">๊ฐ€๊ฒฉ</span>
        </li>
    </ul>
</body>
</html>

๋งˆ์ง€๋ง‰์œผ๋กœ ๋ฉ”์‹œ์ง€ ํŒŒ์ผ์— ์ƒํ’ˆ ์ •๋ณด๋ฅผ ์ถ”๊ฐ€ํ•ด์š”:


# messages.properties
products.title=์ƒํ’ˆ ๋ชฉ๋ก
product.name.1=๋ฉ‹์ง„ ์žฌ๋Šฅ
product.desc.1=์ด ์žฌ๋Šฅ์œผ๋กœ ๋‹น์‹ ์˜ ์‚ถ์„ ๋ณ€ํ™”์‹œํ‚ค์„ธ์š”!
product.name.2=๋†€๋ผ์šด ๊ธฐ์ˆ 
product.desc.2=์ด ๊ธฐ์ˆ ๋กœ ์ƒˆ๋กœ์šด ์„ธ๊ณ„๋ฅผ ์—ด์–ด๋ณด์„ธ์š”!

# messages_en.properties
products.title=Product List
product.name.1=Amazing Talent
product.desc.1=Transform your life with this talent!
product.name.2=Incredible Skill
product.desc.2=Open up a new world with this skill!

# (๋‹ค๋ฅธ ์–ธ์–ด ํŒŒ์ผ๋„ ๋น„์Šทํ•˜๊ฒŒ ์ž‘์„ฑ)

๐Ÿ›’ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ํ•˜๊ธฐ

์žฅ๋ฐ”๊ตฌ๋‹ˆ ๊ธฐ๋Šฅ์„ ๊ฐ„๋‹จํžˆ ๊ตฌํ˜„ํ•ด๋ณผ๊นŒ์š”?

๋จผ์ € ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค์–ด์š”:


@Service
@SessionScope
public class CartService {
    private List<Product> items = new ArrayList<>();

    public void addItem(Product product) {
        items.add(product);
    }

    public List<Product> getItems() {
        return items;
    }
}

๊ทธ๋ฆฌ๊ณ  ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ๋งŒ๋“ค์–ด์š”:


@Controller
@RequestMapping("/cart")
public class CartController {

    @Autowired
    private CartService cartService;

    @GetMapping
    public String viewCart(Model model) {
        model.addAttribute("items", cartService.getItems());
        return "cart";
    }

    @PostMapping("/add")
    public String addToCart(@RequestParam Long productId) {
        // ์‹ค์ œ๋กœ๋Š” ์—ฌ๊ธฐ์„œ ์ƒํ’ˆ์„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์ฐพ์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค.
        Product product = new Product(productId, "product.name." + productId, "product.desc." + productId, new BigDecimal("100.00"));
        cartService.addItem(product);
        return "redirect:/cart";
    }
}

์žฅ๋ฐ”๊ตฌ๋‹ˆ ํŽ˜์ด์ง€ ํ…œํ”Œ๋ฆฟ(cart.html)์„ ๋งŒ๋“ค์–ด์š”:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title th:text="#{cart.title}">์žฅ๋ฐ”๊ตฌ๋‹ˆ</title>
</head>
<body>
    <h1 th:text="#{cart.title}">์žฅ๋ฐ”๊ตฌ๋‹ˆ</h1>
    <ul>
        <li th:each="item : ${items}">
            <span th:text="#{${item.nameKey}}">์ƒํ’ˆ ์ด๋ฆ„</span>
            <span th:text="${#numbers.formatCurrency(item.price)}">๊ฐ€๊ฒฉ</span>
        </li>
    </ul>
    <p th:text="#{cart.total(${#numbers.formatCurrency(#aggregates.sum(items.![price]))})}">์ด์•ก: $300.00</p>
</body>
</html>

๋งˆ์ง€๋ง‰์œผ๋กœ ๋ฉ”์‹œ์ง€ ํŒŒ์ผ์— ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๊ด€๋ จ ๋ฉ”์‹œ์ง€๋ฅผ ์ถ”๊ฐ€ํ•ด์š”:


# messages.properties
cart.title=์žฅ๋ฐ”๊ตฌ๋‹ˆ
cart.total=์ด์•ก: {0}

# messages_en.properties
cart.title=Shopping Cart
cart.total=Total: {0}

# (๋‹ค๋ฅธ ์–ธ์–ด ํŒŒ์ผ๋„ ๋น„์Šทํ•˜๊ฒŒ ์ž‘์„ฑ)

๐ŸŒŸ ๋งˆ๋ฌด๋ฆฌ

์—ฌ๊ธฐ๊นŒ์ง€ Spring Boot๋ฅผ ์‚ฌ์šฉํ•ด ๋‹ค๊ตญ์–ด๋ฅผ ์ง€์›ํ•˜๋Š” ๊ฐ„๋‹จํ•œ ์‡ผํ•‘๋ชฐ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ๋“ค์–ด๋ดค์–ด์š”. ์ด ์˜ˆ์ œ๋ฅผ ํ†ตํ•ด ์šฐ๋ฆฌ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ฒƒ๋“ค์„ ๋ฐฐ์› ์ฃ :

  • ๋ฉ”์‹œ์ง€ ์†Œ์Šค๋ฅผ ์‚ฌ์šฉํ•œ ๋‹ค๊ตญ์–ด ํ…์ŠคํŠธ ๊ด€๋ฆฌ
  • Thymeleaf ํ…œํ”Œ๋ฆฟ์—์„œ ๋‹ค๊ตญ์–ด ๋ฉ”์‹œ์ง€ ์‚ฌ์šฉ
  • ๋™์  ์ฝ˜ํ…์ธ (์ƒํ’ˆ ์ •๋ณด)์˜ ๋‹ค๊ตญ์–ด ์ฒ˜๋ฆฌ
  • ์‚ฌ์šฉ์ž๊ฐ€ ์–ธ์–ด๋ฅผ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ ๊ตฌํ˜„
  • ์ˆซ์ž์™€ ํ†ตํ™”์˜ ์ง€์—ญํ™”๋œ ํฌ๋งทํŒ…

์ด ํ”„๋กœ์ ํŠธ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋” ๋งŽ์€ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๊ณ  ํ™•์žฅํ•  ์ˆ˜ ์žˆ์–ด์š”. ์˜ˆ๋ฅผ ๋“ค์–ด:

  • ์‚ฌ์šฉ์ž ์ธ์ฆ ๋ฐ ํ”„๋กœํ•„ ๊ด€๋ฆฌ
  • ์‹ค์ œ ๊ฒฐ์ œ ์‹œ์Šคํ…œ ์—ฐ๋™
  • ์ƒํ’ˆ ๋ฆฌ๋ทฐ ๋ฐ ํ‰์  ์‹œ์Šคํ…œ
  • ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€๋ฅผ ํ†ตํ•œ ๋™์  ๋‹ค๊ตญ์–ด ์ฝ˜ํ…์ธ  ๊ด€๋ฆฌ

๐ŸŒŸ ์žฌ๋Šฅ๋„ท ๊ฟ€ํŒ: ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์‚ฌ์šฉํ•ด ์ƒํ’ˆ ์ •๋ณด์™€ ๋‹ค๊ตญ์–ด ํ…์ŠคํŠธ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ข‹์•„์š”. ๋˜ํ•œ, ๋ฒˆ์—ญ ๊ด€๋ฆฌ ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ฒˆ์—ญ ํ”„๋กœ์„ธ์Šค๋ฅผ ๋”์šฑ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค!

์ด์ œ ์—ฌ๋Ÿฌ๋ถ„์€ Spring Boot๋ฅผ ์‚ฌ์šฉํ•ด ๋‹ค๊ตญ์–ด๋ฅผ ์ง€์›ํ•˜๋Š” ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ๋งˆ๋ฒ•์‚ฌ๊ฐ€ ๋˜์—ˆ์–ด์š”! ๐Ÿง™โ€โ™‚๏ธโœจ ์ด ์ง€์‹์„ ํ™œ์šฉํ•ด ์—ฌ๋Ÿฌ๋ถ„๋งŒ์˜ ๊ธ€๋กœ๋ฒŒ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณด์„ธ์š”. ์ „ ์„ธ๊ณ„ ์‚ฌ์šฉ์ž๋“ค๊ณผ ์†Œํ†ตํ•˜๋Š” ์—ฌ๋Ÿฌ๋ถ„์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ƒ์ƒํ•ด๋ณด์„ธ์š”. ๋ฉ‹์ง€์ง€ ์•Š๋‚˜์š”? ๐ŸŒ๐Ÿš€

๋‹ค๊ตญ์–ด ์ง€์›์€ ๋‹จ์ˆœํžˆ ๊ธฐ์ˆ ์ ์ธ ๋„์ „์ด ์•„๋‹ˆ๋ผ, ๋ฌธํ™”์  ์ดํ•ด์™€ ์„ธ์‹ฌํ•œ ๋ฐฐ๋ ค๊ฐ€ ํ•„์š”ํ•œ ์ž‘์—…์ด์—์š”. ๊ฐ ์–ธ์–ด์™€ ๋ฌธํ™”์˜ ํŠน์„ฑ์„ ์ดํ•ดํ•˜๊ณ , ์‚ฌ์šฉ์ž๋“ค์—๊ฒŒ ์นœ๊ทผํ•˜๊ณ  ์ž์—ฐ์Šค๋Ÿฌ์šด ๊ฒฝํ—˜์„ ์ œ๊ณตํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด์š”. ์—ฌ๋Ÿฌ๋ถ„์˜ ์„œ๋น„์Šค๊ฐ€ ์ „ ์„ธ๊ณ„ ์‚ฌ์šฉ์ž๋“ค์˜ ๋งˆ์Œ์„ ์‚ฌ๋กœ์žก์„ ์ˆ˜ ์žˆ๊ธฐ๋ฅผ ๋ฐ”๋ผ์š”! ํ™”์ดํŒ…! ๐Ÿ’ช๐Ÿ˜Š