티스토리 뷰

다국적 고객을 대상으로 하는 서비스에서는 사용자 경험을 높이기 위해 다양한 언어로 메세지를 제공해야 합니다. 이 과정에서 MessageSource, Locale, MessageCode와 같은 요소들이 필요합니다. 그러나 이러한 요소로 인해서 다국어 메세지 처리가 애플리케이션에서 문제점을 낳게 되는데, 어떤 문제점이 있는지 어떻게 해결했는지 살펴보도록 하겠습니다.

 

 

개발환경 : Java 11, SpringBoot 2.7.7

1. 문제점 - 의존성 증가와 복잡한 코드


주문 조회 예제 코드

OrdersController 클래스

public class OrdersController {
    private final OrdersService ordersService;
    
    @GetMapping("/order/{ordersUid}")
    public ResponseEntity<OrdersResDto> getOrder(@PathVariable String ordersUid, Locale locale) {            
        // 서비스 클래스에 locale 담아서 보내기
        OrdersResDto dto = ordersService.getOrder(ordersUid, locale);
        
        return ResponseEntity.ok(dto);
    }
}

사용자로부터 주문 조회 요청을 받아 OrdersService를 호출합니다. 이 때, 사용자 언어 설정을 담고 있는 Locale을 함께 전달하게 되는데, Locale을 다른 계층으로 전달해야 하는 부분이 복잡성을 증가시키게 됩니다.

 

OrdersService 클래스

public class OrdersService {
    private final OrdersRepository ordersRepository;
    private final MessageSource messageSource;
    
    public OrdersResDto getOrder(String ordersUid, Locale locale) {
    	// 주문 조회
        OrdersResDto resDto = ordersRepository.findByUid(ordersUid);
        
        // DTO에서 다국어 변환
        resDto.translateOrdersStatus(messageSource, locale);
        
        return resDto;
    }
}

OrdersRepository를 통해 주문 데이터를 조회한 후, MessageSource와 Locale을 서비스 로직에서 DTO로 전달하여 메세지 변환을 수행합니다. translateOrdersStatus(messageSource, locale) 보면 Locale 매개 변수로 담게 되는데 controller -> service ->  dto 까지 3depth 매개 변수로 보내주는 것을 확인할 수 있습니다. 이로 인해 의존성이 증가합니다. 또한 getOrder(ordersUid, locale) 메소드명은 주문 이력을 가져오는 의미인데 적합하지 않는 매개 변수(locale) 받는 것은 잘못된 설계로 이뤄질 수 있습니다.

 

OrdersResDto, OrdersStatus 

public class OrdersResDto {
    ...생략
    private OrdersStatus ordersStatus;
    
    public void translateOrdersStatus(MessageSource messageSource, Locale locale) {
        OrdersStatus ordersStatusEnum = OrdersStatus.valueOf(this.ordersStatus);
        // 다국어 번역
        this.ordersStatus = messageSource.getMessage(ordersStatusEnum.getMessageCode(), null, locale);
    } 
}

public enum OrdersStatus {
    INIT("주문시작", "orders.status.INIT"),
    REQ_PAY("결제요청", "orders.status.REQ_PAY"),
    PAID("결제완료", "orders.status.PAID"),
    FAILED("결제실패", "orders.status.FAILED");

    private String label;
    private String messageCode;

    public String getMessageCode() {
        return messageCode;
    }
}

OrdersResDto 객체에서 주문 상태를 다국어 번역하게 되는데 비즈니스 로직과 관련된 다국어 처리의 세부 사항을 포함하게 되어, 데이터 표현 레이어가 비즈니스 로직에 의존하게 됩니다. 이로 인해 DTO의 역할이 비즈니스 로직과 관련된 처리로 확장되며 복잡성이 증가합니다.

 

 

정리하면

  • OrdersController 클래스 : Locale을 매개 변수로 받아서 다른 계층으로 보내는 문제로 복잡성 증가
  • OrdersService 클래스 :
    • Locale을 controller -> service ->  dto 까지 3depth 매개변수를 보내주고 있어서 의존성 증가
    • 메소드명과 매개 변수가 일치하지 않는 문제점
    • MessageSource를 직접 주입받아 사용해야 했기 때문에 코드의 복잡성과 의존성이 증가
    • 각 클래스에서 메시지 처리를 담당하게 되어 코드 중복이 발생
  • OrdersResDto 클래스 : DTO가 단순히 데이터를 담는 역할을 넘어 다국어 처리 로직을 포함. 이는 DTO의 역할을 확장시켜 코드의 복잡성을 증가

 

 

 

2. 해결 방법


[ 기존 설정 ]

MessageConfig 클래스

public class MessageConfig {
    @Bean
    pubilc ReloadableResourceBundleMessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setDefaultEncoding("UTF-8");
        messageSource.setDefaultLocale(I18nPolicy.DEFAULT_LOCALE);
        messageSource.setBasename(messageLocation);
        messageSource.setCacheMillis(5000);
        return messageSource;
    }
}

기존에는 MessageSource를 직접 빈으로 등록하고, MessageSource를 직접 주입받아 다국어 메시지를 처리했습니다. 그러나 이 방법에는 위 문제점 설명한 것과 같이 의존성 증가와 복잡한 코드 문제점이 있었습니다.

 

 

 

[ 해결 방법 ]

해결 방법으로는 MessageSourceAccessor를 사용합니다. MessageSourceAccessor는 MessageSource의 헬퍼 클래스 역할을 하며, 주요 기능은 다음과 같습니다.

기본 locale 자동 설정

MessageSourceAccessor는 기본 로케일을 자동으로 설정합니다. Locale을 명시적으로 제공하지 않더라도 기본 로케일로 메시지를 처리할 수 있습니다. MessageSourceAccessor 클래스를 보면 getMessage() 코드에서 getDefaultLocale() 기본 로케일을 가져오는 것을 볼 수 있다. 

 

NullPointerException 방지

MessageSourceAccessor는 메시지를 가져오는 과정에서 발생할 수 있는 NullPointerException을 방지하여 더욱 안정적인 메시지 처리가 가능합니다.

 

전역적 메시지 접근

유틸 클래스에 MessageSourceAccessor를 static으로 설정하여 전역적으로 메시지를 접근할 수 있게 됩니다. 이 방식은 메시지 처리를 더 간편하게 해주며, 코드 중복을 줄이고 메시지 관리의 일관성을 높입니다.

 

 

 

 

3. 해결하기


MessageSourceAccessor 주요 기능 중에서 전역적으로 메시지에 접근할 수 있는 기능을 통해, 애플리케이션 내에서 어느 부분에서든 동일한 방식으로 메세지를 처리할 수 있는 유틸리티 클래스를 만들면 유용하겠다는 생각이 들었습니다. 이를 구현하기 위해, 먼저 MessageSource를 MessageSourceAccessor로 감싸는 헬퍼 클래스를 만들었습니다. 그 다음 MessageSourceUtils 유틸리티 클래스와 연계하여, 다국어 메시지 처리를 전역적으로 사용할 수 있도록 하였습니다. 

 

[ MessageSource 유틸 클래스 만들기 ]

MessageConfig 클래스

public class MessageConfig {
    @Bean
    public MessageSourceAccessor messageSourceAccessor() {
        // MessageSourceAccessor를 빈으로 등록하여 전역적으로 사용할 수 있도록 설정
        MessageSourceAccessor messageSourceAccessor = new MessageSourceAccessor(messageSource());
        MessageSourceUtils.setMessageSource(messageSourceAccessor);
        return messageSourceAccessor;
    }

    private ReloadableResourceBundleMessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setDefaultEncoding("UTF-8");
        messageSource.setDefaultLocale(I18nPolicy.DEFAULT_LOCALE);
        messageSource.setBasename(messageLocation);
        messageSource.setCacheMillis(5000);  
        return messageSource;
    }
}

MessageSource를 MessageSourceAccessor 랩핑하여 빈으로 등록하여 메세지 소스를 간편하게 접근할 수 있도록 해줍니다. 그리고 전역적으로 사용하기 위해 MessageSourceUtils 클래스에 MessageSourceAccessor 연계하여 static 메서드를 통해 메시지를 쉽게 조회할 수 있게 됩니다.

 

 

MessageSourceUtils 클래스

public abstract class MessageSourceUtils {

    private static MessageSourceAccessor messageSource;

    public static void setMessageSource(MessageSourceAccessor messageSource) {
        MessageSourceUtils.messageSource = messageSource;
    }

    public static String getMessage(String code) {
        return messageSource.getMessage(code);
    }
    
    public static String getMessage(String code, Object[] args) {
        return messageSource.getMessage(code, args);
    }
}

MessageSourceUtils는 MessageSourceAccessor를 static 변수로 설정하여, 애플리케이션에서 전역적으로 getMessage() 메서드를 통해 다국어 변환을 할 수 있습니다.

 

 

 

4. MessageSourceUtils 사용 방법


1. 생성자에서 다국어 변환

public class OrdersResDto {
    ...생략
    private OrdersStatus ordersStatus;
		
	// 생성자
    public OrdersResDto(OrdersStatus ordersStatus) {
        this.ordersStatus = MessageSourceUtils.getMessage(ordersStatus.getMessageCode());
    }
}

이 방법은 DTO 객체를 생성할 때 메시지를 즉시 변환할 수 있어, 메시지 처리가 필요 없는 상태에서도 안정적인 처리를 보장합니다.

 

2. 메서드를 통한 다국어 변환

public class OrdersResDto {
    ...생략
    private String ordersStatus;

    // 다국어 변환 메서드
    public void translateOrdersStatus() {
        OrdersStatus ordersStatusEnum = OrdersStatus.valueOf(this.ordersStatus);
        this.ordersStatus = MessageSourceUtils.getMessage(ordersStatusEnum.getMessageCode());
    }
}

이 방법은 메서드를 통해서 다국어 변환을 합니다. 객체 생성 후에도 메시지를 변환할 수 있어, 변환이 필요한 시점에 메서드를 호출하여 다국어 처리를 유연하게 수행할 수 있습니다.

 

 

 

 


[ 결론 ]

기존에는 MessageSource를 직접 사용함으로써 코드의 복잡성과 의존성이 증가했습니다. 각 비즈니스 로직에서 MessageSource를 주입받아 메시지 처리를 하다 보니, 코드 중복이 발생하고 유지보수가 어려워졌습니다. 이러한 문제를 해결하기 위해 MessageSourceAccessor와 MessageSourceUtils를 도입했습니다. MessageSourceAccessor는 MessageSource의 헬퍼 클래스로, 메시지 처리의 복잡성을 줄이고, 메시지 조회를 더욱 간편하게 해줍니다. MessageSourceUtils는 static 메서드를 통해 메시지 처리 로직을 중앙 집중화하여, 애플리케이션 전역에서 일관된 메시지 접근을 가능하게 했습니다.