이번 글에서는 Spring Boot에서 다국어 기능을 properties 파일이 아닌 yaml 파일을 이용하여 적용을 해보겠다.

 

다국어 관련 Config 파일 작성.

yml YamlResourceBundle 클래스를 사용하기 위한 pom.xml 파일 혹은 build.gradle 파일에 의존성 추가.

# Maven
<dependency>
  <groupId>net.rakugakibox.util</groupId>
  <artifactId>yaml-resource-bundle</artifactId>
  <version>1.1</version>
</dependency>

# gradle
implementation group: 'net.rakugakibox.util', name: 'yaml-resource-bundle', version: '1.1'

 

Config 클래스 생성

package com.blackvue.config;

import com.blackvue.common.interceptor.WebInterceptor;
import com.navercorp.lucy.security.xss.servletfilter.XssEscapeServletFilter;
import com.pittasoft.common.config.AFConfigYAML;
import net.rakugakibox.util.YamlResourceBundle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.FixedLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;

import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer{

	/**
	 * 변경된 언어 정보를 기억할 로케일 리졸버를 생성한다.
	 * 여기서는 세션에 저장하는 방식을 사용한다.
	 * @return
	 */
	@Bean
	public SessionLocaleResolver localeResolver() {
		SessionLocaleResolver localeResolver = new SessionLocaleResolver();
		localeResolver.setDefaultLocale(Locale.ENGLISH);
		return localeResolver;
	}

	/**
	 * 언어 변경을 위한 인터셉터를 생성한다.
	 */
	@Bean
	public LocaleChangeInterceptor localeChangeInterceptor() {
		LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
		interceptor.setParamName("lang");
		return interceptor;
	}

	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(localeChangeInterceptor())
				.excludePathPatterns("/resources/**", "/resources/", "/css/**", "/vendor/**", "/js/**", "/script/**", "/images/**", "/fonts/**", "/lib/**");
	}

	/**
	 * 각 언어별 메시지 정보를 yaml-bundle을 통해 가져옴
	 */
	@Bean("messageSource")
	public MessageSource messageSource(@Value("${spring.config.messages.basename}") String basename, 
    					@Value("${spring.config.messages.encoding}") String encoding ) {

		YamlMessageSource source = new YamlMessageSource();
		source.setBasename(basename);
		source.setDefaultEncoding(encoding);
		source.setAlwaysUseMessageFormat(true);
		source.setUseCodeAsDefaultMessage(true);
		source.setFallbackToSystemLocale(false);
		return source;
	}

}

class YamlMessageSource extends ResourceBundleMessageSource {
	@Override
	protected ResourceBundle doGetBundle(String basename, Locale locale) throws MissingResourceException {
		return ResourceBundle.getBundle(basename, locale, YamlResourceBundle.Control.INSTANCE);
	}
}

 

src/main/resources/appilcation.yml 에 다국어 설정파일이 있는 경로 추가.

spring:
  config:
    messages:
      basename: lang/messages
      encoding: UTF-8

 

 

src/main/resources/i18n/messages.yml 파일

alert:
  confirm: "확인"
  cancel: "취소"
  loginEmail: "아이디(이메일)를 입력해 주세요."
  loginPassword: "비밀번호를 입력해 주세요."
  loginFail: "아아디(이메일) 또는 비밀번호가 올바르지 않습니다."

src/main/resources/i18n/messages_en.yml 파일

alert: 
  confirm: "Ok" 
  cancel: "Cancel" 
  loginEmail: "Please enter your ID(Email)." 
  loginPassword: "Please enter a password." 
  loginFail: "The email or password is invalid."

src/main/resources/i18n/messages_jp.yml 파일

alert: 
  confirm: "确认" 
  cancel: "取消" 
  loginEmail: "请输入用户名(邮箱)。" 
  loginPassword: "请输入密码。" 
  loginFail: "请重新确认用户名(邮箱)或者密码。"

src/main/resources/i18n/messages_zh.yml 파일.

alert: 
  confirm: "確認" 
  cancel: "取り消し" 
  loginEmail: "ID(Eメール)を入力してください" 
  loginPassword: "パスワードを入力してください" 
  loginFail: "ID(Eメール)またはパスワードをもう一度ご確認ください。"

 

웹페이지에서 사용하기

<!-- Jsp -->
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> <-- 태그 선언 후 

<spring:message code="alert.loginEmail" />

<!-- thymeleaf -->
1. <span th:text="#{alert.loginEmail}">아이디(이메일)를 입력해 주세요.</span>
2. <span>[[#{alert.loginEmail}]]</span>

default url 조회

영문조회 시 (lang=en)

일문 조회시 (lang=jp)

'Framework > Spring Boot' 카테고리의 다른 글

[Spring boot] 다국어 처리하기  (0) 2021.01.05
/* TIME FORMAT */
const FULL      = "yyyy-MM-dd HH:mm:ss.SSS";    // YYYY-MM-DD HH:MM:SS.sss
const LOG       = "MM/dd HH:mm:ss.SSS";         // MMDD HHMMSS.sss
const SEC       = "yyyy-MM-dd HH:mm:ss";     	// YYYY-MM-DD HH:MM:SS
const MIN       = "yyyy-MM-dd HH:mm";       	// YYYY-MM-DD HH:MM
const DATE      = "yyyy-MM-dd";             	// YYYY-MM-DD
const YMDHMSmmm = "yyyyMMddHHmmssSSS";      	// YYYYMMDDHHMMSSmmm
const YMDHMSmm  = "yyyyMMddHHmmssSS";       	// YYYYMMDDHHMMSSmm
const YMDHMS    = "yyyyMMddHHmmss";         	// YYYYMMDDHHMMSS
const YMD       = "yyyyMMdd";               	// YYYYMMDD
const TIME      = "HH:mm:ss.SSS";          	    // HH:MM:SS.sss
const TIME_SEC  = "HH:mm:ss";               	// HH:MM:SS
const TIME_MIN  = "HH:mm";                  	// HH:MM

const weekKorName = ["일요일", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일"];
const weekKorShortName = ["일", "월", "화", "수", "목", "금", "토"];
const weekEngName = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
const weekEngShortName = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const monthEngName = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
const monthEngShortName = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

Date.prototype.format = function (f) {
    if (!this.valueOf()) return " ";

    var d = this;

    return f.replace(/(YYYY|yyyy|YY|yy|MM|MES|MEL|DD|dd|WKS|WKL|WES|WEL|HH|hh|mm|ss|SSS|A\/PK|A\/PE)/gi, function ($1) {
        switch ($1) {
            case "YYYY": case "yyyy": return d.getFullYear();           // 년 (4자리)
            case "YY": case "yy": return (d.getFullYear() % 1000).zf(2);// 년 (2자리)
            case "MM": return (d.getMonth() + 1).zf(2);                 // 월 (2자리)
            case "MES": return monthEngShortName[d.getMonth()];         // 월 (짧은 영어)
            case "MEL": return monthEngName[d.getMonth()];              // 월 (긴 영어)
            case "DD": case "dd": return d.getDate().zf(2);             // 일 (2자리)
            case "WKS": return weekKorShortName[d.getDay()];            // 요일 (짧은 한글)
            case "WKL": return weekKorName[d.getDay()];                 // 요일 (긴 한글)
            case "WES": return weekEngShortName[d.getDay()];            // 요일 (짧은 영어)
            case "WEL": return weekEngName[d.getDay()];                 // 요일 (긴 영어)
            case "HH": return d.getHours().zf(2);                       // 시간 (24시간 기준, 2자리)
            case "hh": return ((h = d.getHours() % 12) ? h : 12).zf(2); // 시간 (12시간 기준, 2자리)
            case "mm": return d.getMinutes().zf(2);                     // 분 (2자리)
            case "ss": return d.getSeconds().zf(2);                     // 초 (2자리)
            case "SSS": return d.getMilliseconds().zf(3);               // 밀리초 (3자리)
            case "A/PK": return d.getHours() < 12 ? "오전" : "오후";    // 오전/오후 구분
            case "A/PE": return d.getHours() < 12 ? "AM" : "PM";        // 오전/오후 구분
            default: return $1;
        }
    });
};

String.prototype.string = function (len) { var s = "", i = 0; while (i++ < len) { s += this; } return s; };
String.prototype.zf = function (len) { return "0".string(len - this.length) + this; };
Number.prototype.zf = function (len) { return this.toString().zf(len); };

 

 

// 2021-01-05 17시 56분 기준
new Date().format("MM월 DD일(WKS) A/PK hh시 mm분");
"01월 05일(화) 오후 5시 56분"

new Date().format(TIME_MIN);
"17:56"

1. MessageSource

국제화 서비스를 위해서는 사용자 언어에 맞는 페이지가 번역되어 보여져야 합니다.

그런데 서비스 되는 언어마다 html 파일을 작성하여, 요청 언어에 따라 응답 페이지를 분기시키는 것은 매우 비효율적이고, 유지보수를 어렵게 만듭니다.

  • ko : index_ko.html
  • jp : index_jp.html
  • cn : index_cn.html
  • en : index_en.html
  • ...

 

그래서 하나의 템플릿만 만들어 놓고 다국어가 처리된 파일을 작성해주면, SpringBoot가 알아서 텍스트를 다국어 처리 할 수 있게끔 해주는 것이 MessageSource입니다.

즉, SpringBoot에서는 src/main/resources/messages.properties를 찾았을 때 자동으로 MessageSource를 구성하고,

해당 파일에 다국어를 작성해주기만 하면, SpringBoot가 알아서 사용자 언어를 확인한 후 메시지를 변환해줍니다.

 

 

2. 구현하기

프로젝트 구조

 

 

 

1) 컨트롤러

src/main/java/com/victolee/i18n/controller/i18nController.java

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class i18nController {

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

 

 

2) 템플릿

src/main/resources/templates/test.html

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
  <h1>i18n 테스트</h1>

  <p th:text="#{product}"></p>
  <p th:text="#{product.category}"></p>
</body>
</html>

thymeleaf에서 MessageSource의 메시지를 가져오기 위해서는 #{속성명}을 작성해주면 됩니다.

즉, product, product.category는 messages.properties 파일에서 언어별로 텍스트를 정의해줄 것입니다.

 

 

3) messages.properties

다음으로 messages_{언어코드}_{국가코드}.properties 파일에 번역 텍스트를 작성해줍니다.

messages.properties 파일은 사용자 언어 환경에 맞는 properties 파일이 없을 경우, 기본 값으로 보여지는 파일입니다.

 

 

각 properties 파일에서 텍스트를 작성하기 전에 IDE에서 *.properties 파일의 인코딩을 UTF-8로 설정해주도록 합니다.

그렇지 않으면 한글 등의 문자가 깨짐 현상이 발생합니다.

 

 

src/main/resources/messages.properties

product=some product
product.category=some category

src/main/resources/messages_en_US.properties

product=us product
product.category=us category

src/main/resources/messages_ko_KR.properties

product=한국 상품
product.category=한국 카테고리

 

각각의 파일은 이렇게 bundle로 묶여지면 손쉽게 관리할 수도 있습니다.

 

여담이지만.. properties 파일이 아닌 yml을 사용하고 싶은데, 현재 다른 포맷을 지원하고 있지 않다고 합니다. ( 참고 )

 

 

 

 

4) application.java

src/main/java/com/victolee/i18n/I18nApplicationjava

package com.victolee.i18n;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;

import java.util.Locale;

@SpringBootApplication
public class I18nApplication {

  public static void main(String[] args) {
  	SpringApplication.run(I18nApplication.class, args);
  }

  /*
  * 사용자 언어 환경을 설정해주기 위한 bean 설정
  */
  @Bean
  public LocaleResolver localeResolver() {
    SessionLocaleResolver sessionLocaleResolver = new SessionLocaleResolver();
    sessionLocaleResolver.setDefaultLocale(Locale.US); // <---- 해당 값을 수정하여 언어 결정
    return sessionLocaleResolver;
  }
}

테스트를 하려면 사용자 언어 환경을 셋팅해줘야 하기 때문에 bean을 추가합니다.

즉, Locale.US로 설정했으면 messages_en_US.properties에 작성된 텍스트가 보여집니다.

아래의 테스트는 해당 값을 바꿔가며 진행됩니다.

 

 

5) 테스트

Locale.US로 설정

messages_en_US.properties 파일에 명시된 텍스트가 의도된대로 노출됩니다.

 

 

 

Locale.JAPAN로 설정

messages_ja_JP.properties 파일은 작성하지 않았기 때문에, 기본 값인 messages.properties 파일의 텍스트가 보여질 것이라 예상할 수 있습니다.

그러나 messages_ko_KR.properties 텍스트가 노출되었는데, 이는 spring.messages.fallback-to-system-locale 설정과 관련이 있습니다.

기본 값이 false이므로 messages_ja_JP.properties 파일을 찾지 못했을 경우, 시스템의 locale을 따라가게 됩니다.

즉, messages_ko_KR.properties 에서 텍스트를 가져오는 것이죠.

( 설정에 대한 내용은 뒤에서 다루겠습니다. )

 

같은 상황에서 spring.messages.fallback-to-system-locale = false 를 해주면 아래와 같이 messages.properties에서 텍스트를 가져옵니다.

 

 

 

6) 설정

예제에서는 다루지 않았지만, MessageSource와 관련된 설정들을 application.yml 파일에 작성할 수 있습니다. ( 참고 )

 key

기본값 

 설명

 spring.messages.always-use-message-format

 false

 MessasgeFormat을 전체 메시지에 적용할 것인지 여부

 spring.messages.basename

 messages

 message 파일을 여러개 사용할 경우 콤마로 구분해서 여러개의 basename을 설정할 수 있다.

 spring.messages.cache-duration

 

 캐시 주기 설정, 기본 값 forever

 spring.messages.encoding

 UTF-8

 인코딩 방식

 spring.messages.fallback-to-system-locale

 true

 감지된 locale에 대한 파일이 없는 경우,

  • 설정값 true : system locale 사용
  • 설정값 false : messages.properties 파일을 사용
 spring.messages.use-code-as-default-message  false

 메시지를 찾지 못했을 때, 예외 처리 대신 메시지 코드를 그대로 반환

 

 

'Framework > Spring Boot' 카테고리의 다른 글

[Spring Boot] yaml로 다국어 기능 구현하기  (0) 2021.01.12

JSON이란.

 

JavaScript Object Notation의 약자

경량의 데이터 교환 형식이라는 뜻이다.

 

데이터의 크기가 작아 XML을 대체해서 데이터 전송 등에 많이 사용한다.

 

이 형식은 사람이 읽고 쓰기에 용이하며, 기계가 분석하고 생성함에도 용이하다.

 

자바 스크립트 언어에 익숙한 사람이라면 다음의 규칙은 익숙할 듯 싶다.

JSON의 문법은 다음과 같다.

 

 - JSON 객체는 중괄호 블록 '{', '}' 으로 표기한다.

 - JSON 배열은 대괄호 블록 '[', ']' 으로 표기한다.

 - 속성(Key)과 값(value)은 쌍을 이룬다.

 - 속성과 값이 쌍을 이룰 때는 콜론 ':'으로 구분한다.

 - 속성이 여러개인 경우 콤마 ',' 로 구분한다.

 - 속성은 쌍따옴표 '"'로 묶어 표기하며, 값은 자료형에 따라 표기한다. 예) "name" : "장진우", "age" : 27

 

   

 

 

JSON 문법을 사용 시 주의해야 할 점 몇가지만 말해볼까한다.

 

 1. JSON은 순수 데이터 포맷이기 때문에 오직 프로퍼티만 담을 수 있다. (Method는 담지 못한다.)

 2. JSON 데이터 구성시 큰 따옴표만을 사용해야한다. (작은 따옴표는 사용불가)

 3. 콤마나 클론을 잘못 배치 할 경우, 문법 오류가 발생할 수 있다.

'공통' 카테고리의 다른 글

[공통] Proxy 설정하기 (Feat. Putty)  (0) 2021.01.05
[공통] log4j 설정 및 적용하기  (0) 2021.01.05

이번 글에서는 maven의 많은 plugin 중 shade-plugin을 이용하여 'uber-jar'를 만들어 보겠다.

 

'uber' 라는 말은 독일어로서, 'over' 라는 뜻이 있다고 한다.

영어로 따지자면 'over-jar' 인 것이다.

 

'uber-jar' 라는 것은 자바 어플리케이션의 모든 패키지와 그에 의존관계에 있는 패키지 라이브러리 까지

모두 하나의 'jar'에 담겨져 있는 것을 말한다.

 

'uber-jar'를 사용하면 어플리케이션을 배포할 때 의존관계를 생각할 필요가 없다.

왜냐하면 이미 필요한 의존관계 라이브러리를 가지고 있기 때문이다.

 

이러한 'uber-jar'를 생성할 때, 해당 어플리케이션의 모든 의존관계까지 포함하다 보니

어플리케이션 배포시에 필요없는 라이브러리까지 몽땅 패키징되는 경우가 있다.

 

shade plugin의 강력함은 배포시에 필요한 라이브러리들을 'exclude/include' 시킬 수 있고,

라이브러리 레벨 뿐만 아니라 class 파일 레벨로도 jar 파일을 minimize 함으로서

보다 가벼운 jar파일을 생성할 수 있다는 것이다.

 

이제부터 shade 플러그인의 사용법을 알아보자.

 

아래는 기본적인 사용법이다.

 

 

 

메이븐 goal로 'shade:shade' 를 입력하여 직접 구동 시킬 수 있지만,

<executions> 설정으로 package phase에 shade goal을 바인딩 하는 설정을 하면

'mvn package' 로 구동 시킬 수 있다.

 

 

Resource Transformer

 

shade 플러그인을 적용할 때 'Resource Transformer' 라는 개념을 이해할 필요가 있다.

 

Resource Transformer 설정을 하면 서로 다른 artifacets 들로부터 uber-jar를 생성할 때,

classes 및 resources 파일들을 중복없이 패키징 할 수 있게 해준다.

 

각 Resource Transformer 설정의 종류 및 특징은 다음과 같다.

 

 

출처 : https://maven.apache.org/plugins/maven-shade-plugin/examples/resource-transformers.html

 

이들 중 흔히 쓰이는 몇가지 설정만 살펴보자.

 

1. ManifestResourcesTransformer

 

여기소 주로 쓰이는 것은 ManifestResourcesTransformer 인데, 

설명대로 자바 'MANIFEST' 파일의 entries를 세팅해 준다.

 

아래와 같이 <configuration> 설정에 추가한다.

 

 

실행 가능한 jar 파일을 생성할시에 자바 어플리케이션을 구동할 MainClass를 지정해야 하는데,

이것은 'MANIFEST' 파일의 entry 중 하나이다.

 

위 예제처럼 <mainclass> 설정으로 해당 어플리케이션의 메인클래스를 입력한다.

 

2. AppendingTransformer

 

만약 스프링 batch 프로젝트를 shade 플러그인을 통해 'Executable Jar'(사용가능한 Jar) 파일로 패키징 한다고 하자.

그럴 경우 Main 클래스는 스프링 batch Job을 커맨드 라인에서 실행 할 수 있게해주는

'org.springframework.batch.core.launch.support.CommandLineJobRunner'가 된다.

 

위의 ManifestResourcesTransformer의 mainClass 설정만 해주면 될까?

그것만 해서는 안된다.

 

스프링으로 구성된 어플리케이션은 spring-context.xml의 namespace를 핸들링 해주는

Handler 클래스들이 정의되어 있는 spring.handlers, 스키마(xsd 등)가 정의되어 있는 spring.schemas 파일이 필요하다.

 

바로 이때, AppendingTransformer 설정을 사용하여 uber-jar에 포함 시킬 수 있다.

 

 

 

기본적으로 스프링 라이브러리 jar 파일을 까보면 각각 META-INF 밑에 spring.handlers, spring.schemas 파일이 존재한다.

 

앞서 설명했듯이 shade 플러그인이 모든 의존관계 라이브러리들을 한데 묶어서 uber-jar를 생성할 때,

위 2개의 파일들이 각각 스프링 라이브러리에 동일한 이름으로 존재하기 때문의 중복의 문제가 존재한다.

 

따라서 AppendingTransformer 설정으로 해당 파일들을 포함시키면, 마치 병합(merge)을 하는 것과 같이

각 라이브러리의 핸들러, 스키마 정보들이 각각 하나의 spring.handlers, spring.shcemas 파일로 생성되는 것이다.

 

나머지 ResourcesTransformer에 대해서는 공식 Document를 참고하자.

 

 

이제 shade 플러그인의 기본적인 설정은 완료되었으니, 프로젝트를 패키징 해보자.

 

패키징이 완료가 되면 ${project.basedir}/target에 XXX.jar 파일이 생성이 되었을 것이다.

 

cmd창을 열어 해당 위치로 접근한 후, java -jar XXX.jar 명령어를 실행하게 되면, jar파일이 실행될 것이다.



다음은 실행한 화면이다.

 

 

pom.xml에서 라이브러리의 의존성을 설정하는 부분에서 <scope>의 값을 system으로 설정을 하였다면,

shade plugin에서 해당 라이브러리를 include 하지 않아서 

XXX.jar를 실행 시에 ClassNotFoundException 발생할 가능성이 있다.

 

다음 글에서는 <scope>의 값을 system로 설정하였더라도

패키징시에 해당라이브러리 까지 포함하는 Jar를 만드는 방법을 기술하겠다.

 

 

 

+ Recent posts