본문 바로가기

백앤드/Java

[JAVA] SpringBoot와 Resource Bundle로 다국어(i18n) 로그 관리하기

반응형

직접 Logback과 Resource Bundle를 사용해서 로그 관리하는 방법을 기존에 포스팅을 했었습니다.

[SpringBoot/Java] Logback과 Resource Bundle 로 로그 관리하기 :: 개발자 보물상자 (tistory.com)

 

[SpringBoot/Java] Logback과 Resource Bundle 로 로그 관리하기

서론 없이 바로 본론으로 들어가겠습니다. 1. Maven (pom.xml) Dependency 설정 ch.qos.logback logback-core 1.2.9 ch.qos.logback logback-classic 1.2.9 org.slf4j slf4j-api 1.7.32 org.slf4j slf4j-ext 1.7.32 ch.qos.cal10n cal10n-api 0.8.1 2. Res

dev-box.tistory.com

 

이번에는 SpringBoot를 사용하는 경우, SpringBoot에 이미 slf4j 라이브러리를 포함하고 있기 때문에 별도의 라이브러리를 하지 않고, 로그 관리하는 방법을 소개해봅니다.

 

로그는 다국어를 지원하기 위한 i18n 이라고 지칭하기도 합니다.

 

1. 다국어 로그 환경 설정 클래스 생성

/**
 * This class can call information from files set in application.yaml.
 *
 * @author jskang
 */
public class LocaleValue {

    protected static String locale = "ko_kr";
    protected static String[] basename = new String[]{"messages/log"};
    protected static String encoding = "UTF-8";

    /**
     * Load set locale information.
     *
     * @return the locale.
     * @author jskang
     */
    public static String getLocale() {
        return locale;
    }

    /**
     * Load set basename path.
     *
     * @return the basenames.
     * @author jskang
     */
    public static String[] getBasename() {
        return basename;
    }

    /**
     * Load set encoding information.
     *
     * @return the encoding.
     * @author jskang
     */
    public static String getEncoding() {
        return encoding;
    }
}

 

2. 지원하는 다국어 목록 정의 클래스 생성

import java.util.Arrays;
import java.util.Locale;

/**
 * A predefined class with local information that can be set.
 * Regions not provided by this class are not supported.
 *
 * @author jskang
 */
public enum LocaleType {
    KOKR("ko", "kr"),
    ENUS("en", "us");

    private String language;
    private String country;

    LocaleType(String l, String c) {
        this.language = l;
        this.country = c;
    }

    /**
     * You can return a Locale class for any country you want.
     *
     * @param language the language you want.
     * @param country city you want.
     *
     * @return Locale class.
     * @author jskang
     */
    public static Locale getLocale(String language, String country) {
        return Arrays.stream(values())
            .filter(locales -> locales.language.equals(language) && locales.country.equals(country))
            .findFirst()
            .map(localeType -> new Locale(localeType.language, localeType.country))
            .orElse(new Locale("ko", "kr"));
    }

    /**
     * You can return a Locale class for any country you want.
     * default locale is korean.
     *
     * @return Locale class.
     * @author jskang
     */
    public static Locale getLocale() {
        String locale = LocaleValue.locale;
        if (locale == null || locale.length() == 0) {
            return new Locale("ko", "kr");
        }

        String[] split = locale.split("_");
        if (split.length != 2) {
            return new Locale("ko", "kr");
        }

        return getLocale(split[0], split[1]);
    }

    /**
     * You can return a Locale class for any country you want.
     *
     * @param localeType Value provided by LocaleType Enum class.
     *
     * @return Locale class.
     * @author jskang
     */
    public static Locale getLocale(LocaleType localeType) {
        return getLocale(localeType.language, localeType.country);
    }

    /**
     * You can return a Locale class for any country you want.
     *
     * @param locale the language you want and city you want. (ex. ko_kr | en_us)
     *
     * @return Locale class.
     * @author jskang
     */
    public static Locale getLocale(String locale) {
        if (locale == null || locale.length() == 0) {
            return new Locale("ko", "kr");
        }

        String[] split = locale.split("_");
        if (split.length < 2) {
            return new Locale("ko", "kr");
        }

        return getLocale(split[0], split[1]);
    }

    /**
     * You can return a LocaleType class for any country you want.
     *
     * @param language the language you want.
     * @param country city you want.
     *
     * @return LocaleType class.
     * @author jskang
     */
    public static LocaleType getLocalType(String language, String country) {
        return Arrays.stream(values())
            .filter(locales -> locales.language.equals(language) && locales.country.equals(country))
            .findFirst()
            .orElse(LocaleType.KOKR);
    }

    /**
     * You can return a LocaleType class for any country you want.
     *
     * @return LocaleType class.
     * @author jskang
     */
    public static LocaleType getLocalType() {
        String locale = LocaleValue.locale;
        if (locale == null || locale.length() == 0) {
            return LocaleType.KOKR;
        }

        String[] split = locale.split("_");
        if (split.length != 2) {
            return LocaleType.KOKR;
        }

        return getLocalType(split[0], split[1]);
    }

    /**
     * You can return a LocaleType class for any country you want.
     *
     * @param locale the language you want and city you want. (ex. ko_kr | en_us)
     *
     * @return LocaleType class.
     * @author jskang
     */
    public static LocaleType getLocalType(String locale) {
        if (locale == null || locale.length() == 0) {
            return LocaleType.KOKR;
        }

        String[] split = locale.split("_");
        if (split.length < 2) {
            return LocaleType.KOKR;
        }

        return getLocalType(split[0], split[1]);
    }
}

 

3. SpringBoot Bean 생성

import java.util.Locale;
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.i18n.LocaleContext;
import org.springframework.context.i18n.SimpleLocaleContext;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.i18n.LocaleContextResolver;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;

@Configuration
public class LocaleConfig implements WebMvcConfigurer, LocaleContextResolver {

    @Value("${locale.language}")
    private String locale;
    @Value("${locale.messages.encoding}")
    private String encoding;

    @Bean
    public LocaleResolver localeResolver() {
        LocaleValue.locale = locale;
        CookieLocaleResolver cookieLocaleResolver = new CookieLocaleResolver();
        cookieLocaleResolver.setDefaultLocale(LocaleType.getLocale(LocaleValue.locale));
        cookieLocaleResolver.setCookieName("APPLICATION_LOCALE");
        return cookieLocaleResolver;
    }

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

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

    @Override
    public LocaleContext resolveLocaleContext(ServerWebExchange exchange) {
        String language = exchange.getRequest().getQueryParams().getFirst("lang");
        Locale targetLocale = Locale.getDefault();
        if (language != null && !language.isEmpty()) {
            targetLocale = Locale.forLanguageTag(language);
        }
        return new SimpleLocaleContext(targetLocale);
    }

    @Override
    public void setLocaleContext(ServerWebExchange exchange, LocaleContext localeContext) {
        throw new UnsupportedOperationException("Not Supported");
    }

    @Bean
    public MessageSource messageSource() {
        LocaleValue.basename = new String[]{"messages/log"};
        LocaleValue.encoding = encoding;

        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.addBasenames(LocaleValue.basename);
        messageSource.setDefaultEncoding(LocaleValue.encoding);
        messageSource.setUseCodeAsDefaultMessage(true);
        messageSource.setCacheSeconds(60 * 100);
        return messageSource;
    }
}

 

4. application.properties에서 SpringBoot 옵션 설정

locale:
  language: ko_kr
  messages:
    encoding: UTF-8

 

5. ResourceBundle 로그 메세지 정의 문서 생성

해당 파일은 resources 디렉토리에 생성해야합니다.

구체적으로 설명하면 3. SpringBoot Bean 생성 코드 중 messageSource() 함수에 정의된 LocaleValue.basename 에 있는 경로를 생성해야합니다.

즉, resources/messages/log.ko_kr.properties 이런식의 포맷으로 생성해야합니다. 그렇지 않으면 로그가 정상적으로 출력되지 않습니다.

 

5-1. 한국어 (log.ko_kr.properties)

L000000={0}

L000001=데이터를 조회합니다.
L000002=데이터 조회에 실패하였습니다. 원인: {0}
L000003=데이터 조회 시간이 너무 오래걸립니다.
L000004={0} 데이터를 조회하였습니다.
L000005=데이터를 삭제합니다.
L000006=데이터 삭제에 실패하였습니다. 원인: {0}
L000007={0} 데이터를 삭제하였습니다. 이유: {1}
L000008=데이터 검증에 실패하였습니다. 원인: {0}
L000009=저장된 데이터가 존재하지 않습니다.

5-2. 영어 (log.en_us.properties)

L000000={0}

L000001=Query data.
L000002=Failed to retrieve data. Cause: {0}
L000003=Data lookup is taking too long.
L000004=Searched {0} data.
L000005=Clear data.
L000006=Failed to delete data. Cause: {0}
L000007=Data deleted {0}. Reason: {1}
L000008=Data validation failed. Cause: {0}
L000009=The saved data does not exist.

 

6. 로그 코드를 정의하는 Enum 클래스 생성

/**
 * This is an Enum class that manages log codes to be output to the console log.
 * Add as many log codes as you need, and also add them to Bundle path in resources/message.
 *
 * @author jskang
 */
public enum LogCode implements Code {
    L000000,
    L000001,
    L000002,
    L000003,
    L000004,
    L000005,
    L000006,
    L000007,
    L000008,
    L000009;
}

 

7. 로그 실제 사용 예제

코드는 매우 더러울겁니다. 왜냐, 깔끔하게 정리된 제 코드는 외장하드에만... 필요하시면 필자를 데려가서 고용하시면 될 것 같습니다. (자본주의에 찌듦...)

import java.util.List;
import java.util.Map;
import kr.co.wisenut.sampleproject.common.code.LogCode;
import kr.co.wisenut.sampleproject.config.i18n.LocaleType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value = "sample")
public class SampleController {

    Logger log = LoggerFactory.getLogger(getClass());

    @Autowired
    private MessageSource messageSource;
    
    @PostMapping(value = "data")
    public ResponseEntity create(@RequestBody Map<String, String> data, @RequestParam(required = false) String lang) {
        log.info(messageSource.getMessage(LogCode.L000004.name(), new String[]{data.toString()}, LocaleType.getLocale()));
        return ResponseEntity.ok().build();
    }
}

 

코드를 쪼개어놔서 많아보이지만, 실제로 샘플 프로젝트로 만들어보면 코드는 정말 간단합니다.

여기까지 포스팅 끝-

반응형