[Spring MessageSource] 자동설정으로 MessageSource 쉽게 세팅하기

김유정·2023년 1월 17일
1
post-thumbnail

Spring으로 다국어를 처리하는 방법에 대해 검색해보면, Spring Boot를 사용하는 경우에는 MessageSource가 자동으로 등록되기 때문에 등록하지 않아도 되고, Spring Boot를 사용하지 않는 경우에는 아래와 같이 설정파일에 MessageSource를 등록하라는 글이 많이 나온다.

@Configuration
public class MessageConfig {
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasenames("customMessage/messages");
        messageSource.setFallbackToSystemLocale(false);
        messageSource.setDefaultEncoding("UTF-8");

        return messageSource;
    }
}

근데 Spring Boot를 사용하면 어떻게 자동 등록이 되는건지, 기본 세팅된 설정은 어떻게 되는지, 그 설정을 바꿀수 있는건지 이런 부분에 대해서 명확하게 설명한 글은 찾기 어려워서 힘들었다. 그래서 지금까지 알아낸 부분에 대해 정리해보고자 한다.

MessageSource 자동설정 하는 방법

MessageSource 속성 및 기본 설정

속성타입설명기본값
basenameString- 쉼표로 구분된 basename 리스트"messages"
encodingCharset- 메세지 인코딩 방식StandardCharsets.UTF_8
cacheDurationDuration- 로드된 리소스 번들 파일 캐시 기간.
- 단위를 지정하지 않으면 초가 사용됨.
영구적으로 캐시됨
fallbackToSystemLocaleboolean- 요청받은 Locale에 대한 파일을 찾지 못할 경우 시스템 설정 Locale을 사용할 것인지에 대한 유무true
alwaysUseMessageFormatboolean- 전달받은 인자를 제외하고 메세지를 읽어들이는 MessageFormat 규칙을 항상 적용할 것인지에 대한 유무false
useCodeAsDefaultMessageboolean- 메세지 파일에 코드에 대해당하는 값이 없을 때 NoSuchMessageException 대신 코드를 메세지로 사용할 것인지에 대한 유무false

설정 변경 없이 사용하는 법

  1. 설정
    위의 표에서 나타낸 것처럼 기본적으로 세팅이 되어 있기 때문에, 변경할 필요가 없다면 설정과 관련해서 아무것도 안해도 된다. 메세지 파일만 만들어주면 된다.

  2. 메세지 파일 만들기
    basename이 "messages"이기 때문에, resources 폴더 바로 아래 messages.properties 파일을 만들어주면 된다. 다른 locale에 대한 메세지 파일을 만들고 싶다면, 같은 위치에 messages_en.properties 이런식으로 만들어주면 된다. 메세지 파일 안에는 success=성공! 과 같이 키=값 형태로 메세지를 정리해두면된다.

설정을 커스텀하여 사용하는 법

  1. 설정
    메세지 파일의 위치를 바꾸고 싶거나 인코딩 방식 등 기본 설정을 변경하고 싶다면, application.properties 이나 application.yml 파일에 속성을 정의해주면 된다. 이게 끝이다. 설정파일을 만들어서 MessageSource를 만들고 bean으로 등록하지 않아도 된다.
  • application.properties 예시
spring.messages.basename=message/messages
spring.messages.encoding=UTF-8
spring.messages.fallbackToSystemLocale=false
spring.messages.alwaysUseMessageFormat=true
  • application.yml 예시
spring:
  messages:
    basename:message/messages
    encoding:UTF-8
    fallbackToSystemLocale:false
    alwaysUseMessageFormat:true
  1. 메세지 파일 만들기
    basename에 정의한대로 적절한 위치에 메세지 파일을 만들면 된다. 예를 들어 위의 예시처럼 설정했다면, resources 폴더 아래 messase 폴더를 만들고 거기에 messages로 시작하는 메세지 파일들을 만들면 된다.

자동 설정 동작 과정


application.properties로 자동설정은 했는데, 어떻게 이게 가능한걸까?

메인메서드가 있는 application 클래스부터 살펴보자. 여기서 주목할 건 @SpringBootApplication 어노테이션이다.

@SpringBootApplication
public class ExampleApplication {

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

}

해당 어노테이션 클래스로 넘어가보자.

  • org.springframework.boot.autoconfigure.SpringBootApplication.java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {...}

자동 설정을 위한 @EnableAutoConfiguration 어노테이션과, component를 찾아 bean으로 등록하기 위한 @ComponentScan 어노테이션이 있다.
→ 따라서 굳이 MessageSource를 만들어서 bean으로 등록하지 않아도 자동 설정으로 등록된 bean을 사용할 수 있게 되는 것이다.

그럼 자동 설정되는 MessageSource는 기본적으로 어떤 속성들을 가지고 있을까?

인텔리제이에서 shift 버튼을 두 번 눌러서 AutoConfiguration을 검색해보면 SpringBoot에서 자동설정을 위해 제공하는 파일들을 확인할 수가 있다.

여기서 messageSourceAutoConfiguration.java 파일로 들어가보면 아래와 같이 2개의 Bean과 조건 확인을 위한 ResourceBundleCondition 클래스로 구성되어있다.

@AutoConfiguration
@ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, search = SearchStrategy.CURRENT)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Conditional(ResourceBundleCondition.class)
@EnableConfigurationProperties
public class MessageSourceAutoConfiguration {

	private static final Resource[] NO_RESOURCES = {};

	@Bean
	@ConfigurationProperties(prefix = "spring.messages")
	public MessageSourceProperties messageSourceProperties() {
		return new MessageSourceProperties();
	}

	@Bean
	public MessageSource messageSource(MessageSourceProperties properties) {
		ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
		
        ...
        
		return messageSource;
	}

	protected static class ResourceBundleCondition extends SpringBootCondition {

		private static ConcurrentReferenceHashMap<String, ConditionOutcome> cache = new ConcurrentReferenceHashMap<>();

		@Override
		public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {...}

		private ConditionOutcome getMatchOutcomeForBasename(ConditionContext context, String basename) {...}

		private Resource[] getResources(ClassLoader classLoader, String name) {...}

	}

}

자 이제 하나씩 뜯어보자!!

자동설정을 위한 조건 확인

MessageSourceAutoConfiguration에는 @Conditional(ResourceBundleCondition.class) 어노테이션이 붙어있는데, 인자로 넘어온 클래스가 가지고 있는 matches 메서드가 true를 반환해야 자동설정을 진행한다.
ResourceBundleCondition 클래스의 getMatchOutcome 메서드를 통해 조건 체크를 한다.

getMatchOutcome

@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
	String basename = context.getEnvironment().getProperty("spring.messages.basename", "messages"); // 1. basename 세팅
	ConditionOutcome outcome = cache.get(basename); // 2. 캐시에 basename으로 저장된 값이 있는지 확인
	if (outcome == null) {
		outcome = getMatchOutcomeForBasename(context, basename); //  3. 저장된 값이 없다면 getMatchOutcomeForBasename 호출
		cache.put(basename, outcome); // 4. 결과값 cache 변수에 저장
	}
	return outcome;
}
  1. basename을 세팅한다. application.properties 또는 application.yml에서 spring.messages.basename 속성에 대한 값을 가져온다. 없다면, messages로 할당한다.
  2. 캐시에 basename으로 저장된 값이 있는지 확인
  3. cache 라는 변수에 저장된 값이 없다면, getMatchOutcomeForBasename 을 호출하고 결과값을 outcome에 할당한다.
  4. outcome을 cache 변수에 저장한다.

getMatchOutcomeForBasename

private ConditionOutcome getMatchOutcomeForBasename(ConditionContext context, String basename) {
	ConditionMessage.Builder message = ConditionMessage.forCondition("ResourceBundle");
	for (String name : StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(basename))) { // 1. basename 리스트 돌면서 getResources를 호출
		for (Resource resource : getResources(context.getClassLoader(), name)) {
			if (resource.exists()) { // 2. getResources의 결과값인 Resource를 확인하면서 존재 유무 확인
				return ConditionOutcome.match(message.found("bundle").items(resource)); // 3. 존재한다면, 아래 사진처럼 match가 true인 outcome을 반환
			}
		}
	}
	return ConditionOutcome.noMatch(message.didNotFind("bundle with basename " + basename).atAll()); // 4. 존재하는 자원이 없다면, match가 false인 outcome을 반환한다.
}
  1. basename 리스트를 만들고 돌면서 getResources에 해당 basename을 담아 호출한다.
  2. getResources의 결과 값인 Resource를 exists 메서드로 존재유무를 확인한다.
  3. 존재하는 자원이 있다면, 아래 사진처럼 match가 true인 outcome을 반환한다.
  4. 존재하는 자원이 없다면, match가 false인 outcome을 반환한다.

getResources

private Resource[] getResources(ClassLoader classLoader, String name) {
	String target = name.replace('.', '/');
	try { // 클래스패스 내에 basename에 해당하는 properties 파일이 있는지 찾아서 반환한다.
		return new PathMatchingResourcePatternResolver(classLoader)
						.getResources("classpath*:" + target + ".properties");
	}
	catch (Exception ex) {
		return NO_RESOURCES;
	}
}

클래스패스 내에 basename에 해당하는 properties 파일이 있는지 찾아서 반환한다.

🔽 여기서 알 수 있는 중요한 사실 2 가지가 있다 🔽

클래스패스 내에 basename에 해당하는 properties 파일이 없다면 자동 설정이 안된다. 따라서 사용하지 않더라도 messages.properties와 같은 기본 파일은 만들어두는 게 좋을 듯 하다.

메세지파일을 yml 형식으로 관리하는 건 불가하다. 현재 Spring Boot에서 공식적으로 지원하는 건 properties 형식이다.

yml로 관리하기 위한 라이브러리가 있긴하지만, 관련 문서가 적고 에러가 나도 해결하기 까다로울 듯 하다.

MessageSource 속성 설정

messageSourceAutoConfiguration.java에서 아래 코드 부분은 MessageSource의 속성을 설정하는 부분이다.

	@Bean
	@ConfigurationProperties(prefix = "spring.messages")
	public MessageSourceProperties messageSourceProperties() {
		return new MessageSourceProperties();
	}

@ConfigurationProperties 어노테이션은 application.propertiesapplication.yml 파일에서 prefix로 시작하는 속성을 찾아서 객체에 바인딩 해주는 역할을 한다. (@Value 어노테이션과 비슷한 역할이지만, 여러 속성을 한 번에 그리고 유연하게 바인딩할 수 있다.)

@ConfigurationProperties(prefix = "spring.messages")이기 때문에, 위에서 작성한 예시처럼 application.properties나 application.yml 에 spring.messages의 속성을 설정할 수 있는 것이다.

MessageSource 만들기

messageSourceAutoConfiguration.java에서 아래 코드 부분은 Bean으로 등록할 MessageSource를 만드는 부분이다.

    @Bean
	public MessageSource messageSource(MessageSourceProperties properties) {
		ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
		if (StringUtils.hasText(properties.getBasename())) {
			messageSource.setBasenames(StringUtils
					.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename())));
		}
		if (properties.getEncoding() != null) {
			messageSource.setDefaultEncoding(properties.getEncoding().name());
		}
		messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
		Duration cacheDuration = properties.getCacheDuration();
		if (cacheDuration != null) {
			messageSource.setCacheMillis(cacheDuration.toMillis());
		}
		messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
		messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
		return messageSource;
	}

새로운 MessageSource를 만들고 위에서 만든 MessageSourceProperties의 값들을 MessageSource에 세팅한다.

직접 만든 MessageSource VS 자동설정으로 만들어지는 MessageSource

마지막으로 한 가지 질문에 대해 더 살펴보려고 한다.

설정 파일에 MessageSource도 만들고, application.properties에 자동설정 세팅도 해놓으면 어떤 메세지를 선택할까?

테스트 환경

1. resources 아래 두 개의 폴더를 만들어줬다. 하나는 커스텀한 MessageSource에서 사용할 메세지 파일이고, 하나는 자동설정에서 사용할 메세지 파일이다.

2. 설정 파일을 생성하고 MessageSource를 아래와 같이 만들었다.

@Configuration
public class MessageConfig {
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasenames("customMessage/messages");
        messageSource.setFallbackToSystemLocale(false);
        messageSource.setDefaultEncoding("UTF-8");

        return messageSource;
    }
}

3. application.properties에는 아래와 같이 자동설정을 위한 값들을 추가했다.

spring.messages.basename=message/messages
spring.messages.encoding=UTF-8
spring.messages.fallbackToSystemLocale=false
  1. 테스트를 할 컨트롤러도 만들었다. MessageSource 의존성을 주입받고, "hello"에 해당하는 메세지를 가져올 것이다.
@RequiredArgsConstructor
@RestController
public class testController {

    private final MessageSource messageSource;

    @GetMapping()
    public ResponseEntity<String> printHello() {
        return ResponseEntity.status(200).body(messageSource.getMessage("hello", null, LocaleContextHolder.getLocale()));
    }
}

5. 테스트


테스트를 해봤더니, 커스텀한 MessageSource의 메세지를 가져오는 걸 확인할 수 있었다.
그 이유는 MessageSourceAutoConfiguration 에 있는 @ConditionalOnMissingBean 어노테이션 때문이다.
해당 어노테이션은 특정 bean이 없어야 조건을 만족시키는 어노테이션인데, MessageSource를 직접 bean으로 만들어버리면 저 조건을 만족시키지 못한다.
즉 MessageSource bean을 직접 만들지 않을 때에만 자동설정된 MessageSource bean을 사용할 수 있다.

@AutoConfiguration
@ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, search = SearchStrategy.CURRENT)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Conditional(ResourceBundleCondition.class)
@EnableConfigurationProperties
public class MessageSourceAutoConfiguration {...}

참고

0개의 댓글