DB를 통한 다국어 지원 기능 구현

Hyunsoo Kim·2024년 5월 17일
0

스프링

목록 보기
6/13
post-thumbnail
post-custom-banner

한동안 다국어 지원 기능을 변경하는 부분에 관해 고민하다가 좋은 문서가 있어서 기록해 두려고 한다.

Database-Stored Messages for I18n in Spring Boot

스프링 다국어 지원을 검색하다보면 properties로 해결한 경우를 굉장히 많이 볼 수 있다. 그러나 경우에 따라서는 DB에 데이터를 넣어두고 가져오는 방식으로 구현해야 할 때가 있다.

그렇다면 DB로 다국어 지원을 어떻게 구현할 수 있을까?


Dependency

사용하는 툴에 맞게 maven이나 gradle 의존성부터 추가하자.

문서는 Spring Boot, Thymeleaf, Spring Data JPA, H2를 사용했는데, 나는 이것 모두 사용하지 않아서 따로 의존성을 주입하진 않았다.

각자 사용하는 부분에 맞춰 커스텀해서 사용하면 될 듯하다.

<dependencies>
    <dependency>
        <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    </dependency>
    <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>
    <dependency>
        <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
</dependencies>

Database

문서에서는 id, locale, messagekey, messageContent로 컬럼을 구성했다. 다만, 나는 이 역시 DB 구성을 다르게 짰다.

다국어 지원 DB 구성에 관해서 깊게 다루신 분의 글이 있어서 태그해둔다. 나는 이분의 예시 중 하나를 선택해서 DB 테이블과 컬럼을 구성했다.

다국어 지원을 위한 데이터베이스 설계와 Spring Boot 구현 전략


Entity And Repository

사용하는 DB와 테이블 구성에 맞게 처리하면 된다. 문서는 JPA를 기반으로 Entity와 Repository를 개발했다.

@Entity

@Table(name = "languages")

public class LanguageEntity {

    @GeneratedValue(strategy = GenerationType.AUTO)

    @Id

    @Column

    private Integer id;

    @Column

    private String locale;

    @Column(name = "messagekey")

    private String key;

    @Column(name = "messagecontent")

    private String content;

  //Getter & Setter

}
@Repository

public interface LanguageRepository extends JpaRepository<LanguageEntity, Integer> {

    LanguageEntity findByKeyAndLocale(String key, String locale);

}

I18N with the Database-Stored Message

여기서부터 본격적으로 다국어 지원 기능을 개발하게 된다.

로직을 이미지화하면 다음과 같다.

1) JSP 특정 코드를 통해 URL에 '?lang={각 언어}"를 포함시킨다.
2) 인터셉터가 'lang'을 인식하면 MessageSource로 넘긴다.
3) 커스터마이징된 MessageSource가 번역 로직을 수행하고, DB에서 데이터를 찾아와 매칭한다.
4) 찾아온 데이터를 JSP에 출력한다.

LocaleResolver and LocaleChangeInterceptor

인터셉터 구성은 locale 관련한 내용은 'cookie'에 넣어 관리하고, 인터셉터를 통해 URL 속의 'lang'을 감지하는 로직이다.

@Configuration

public class WebConfig implements WebMvcConfigurer {

    @Bean

    public LocaleResolver localeResolver() {

        return new CookieLocaleResolver();

    }

    @Override

    public void addInterceptors(InterceptorRegistry registry) {

        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();

        localeChangeInterceptor.setParamName("lang");

        registry.addInterceptor(localeChangeInterceptor);

    }

}

쿠키냐, 세션이냐?

로직을 구성할 때 localeResolver를 위해 쿠키를 사용할지, 세션을 사용할지 고민이 많았다.

쿠키클라이언트(브라우저/개인 컴퓨터) 로컬에 저장되는 키와 값이 들어있는 작은 데이터 파일이다. 클라이언트 상태 정보를 로컬(브라우저)에 저장 후 참조하며, 사용자가 따로 요청하지 않아도 브라우저가 Request 시에 Request Header를 넣어 자동으로 서버에 전송한다.

세션은 쿠키를 기반으로 하고 있지만, 사용자 정보 파일을 서버(웹사이트) 측에서 관리한다. 사용자에 대한 정보를 서버에 두기 때문에 쿠키보다 보안에 좋지만 사용자가 많아질수록 서버 메모리를 많이 차지하게 된다.

결론적으로 나는 쿠키를 사용해서 처리했다. 만일 @Configuration에서 직접 데이터를 다루는 로직이 있다면, 세션 처리를 하는 것이 바람직할 것이다. 하지만 localeResolver는 웹 요청과 관련된 Locale 객체를 추출하고, 이를 이용하는 역할만 수행하기 때문에 보안보다는 속도와 성능을 우선시하는 게 좋다고 생각했다.

특히 다국어 지원 기능은 속도가 중요한데, DB에서 데이터를 가져오기 때문에 이미 저하된 속도에 세션으로 부하를 줄 필요는 없다고 판단했다.

세션을 사용한 예제는 다음과 같다.

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

WebCofig로는 적용이 안 될 때

WebCofig에 interceptor를 구현했으나 적용이 안 될 때가 있다. 이때는 servlet-context에서 다음과 같은 코드를 추가하면 해결될 것이다.

<interceptors>
		<beans:bean id="localeChangeInterceptor" class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
			<beans:property name="paramName" value="lang" />
		</beans:bean>
	</interceptors>

Custom DBMessageSource

MessageSource을 다국어지원 기능에 맞춰 커스터마이징해야 한다.
이 부분은 DB를 어떻게 구성했느냐에 따라 완전히 달라진다. 내가 개발한 DBMessageSource는 문서의 클래스와 판이하게 달랐다.

@Component("messageSource")

public class DBMessageSource extends AbstractMessageSource {

    @Autowired

    private LanguageRepository languageRepository;

    private static final String DEFAULT_LOCALE_CODE = "en";

    @Override

    protected MessageFormat resolveCode(String key, Locale locale) {

        LanguageEntity message = languageRepository.findByKeyAndLocale(key,locale.getLanguage());

    if (message == null) {

        message = languageRepository.findByKeyAndLocale(key,DEFAULT_LOCALE_CODE);

    }

    return new MessageFormat(message.getContent(), locale);

    }

}

Controller and View

컨트롤러와 뷰를 만들어 화면에서 다국어 지원 기능이 제대로 출력되는지 확인할 수 있다.

Controller

@Controller

public class HomeController {

    @RequestMapping("/")

    public String welcome(Map<String, Object> model) {

        return "index";

    }

}

다국어 지원 기능은 인터셉터에서 URL을 잡아 처리하기 때문에, 컨트롤러에서 DBMessageSource를 사용할 필요가 없다.

View

문서는 Thymeleaf를 통해 뷰 단에 데이터를 뿌린다.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>I18N Spring Boot</title>
</head>
<body>
    <h2 data-th-text="#{home.welcome}"></h2>
    <p data-th-text="#{home.info}"></p>
    <p data-th-text="#{home.changelanguage}"></p>
    <ul>
        <li><a href="?lang=en" data-th-text="#{home.lang.en}"></a></li>
        <li><a href="?lang=de" data-th-text="#{home.lang.de}"></a></li>
        <li><a href="?lang=zh" data-th-text="#{home.lang.zh}"></a></li>
    </ul>
</body>
</html>

나는 spring:message를 통해 jsp로 처리했다.

<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>

<html>

<head>

(생략)

</head>

<body>

<h1><spring:message code='(찾아올 데이터 키 값)'/></h1>

</body>

</html>

Cache

DB를 통해 다국어를 지원하면 spring:message의 수만큼 select문이 실행된다.
데이터 표본이 적다면 상관 없지만, 서비스가 커지면서 데이터가 늘어나면 자연히 성능이 저하될 수 있다.

이 문제를 해결하기 위해 Repository에 캐시를 적용했다.

캐시를 사용하면, DB를 한 번만 호출하고 나머지 작업을 처리할 때는 캐시에서 데이터를 받아오기에 성능과 속도 면에서 향상된 것을 확인할 수 있다.

단, DB가 변경되어도 캐시로 인해 화면엔 즉각 반영되지 않을 수 있다. 그러므로 DB가 변경되었을 때는 이벤트를 통해 캐시를 비우고 다시 넣는 로직이 필요하다.


다국어 지원은 서비스가 확장될 때 기본적으로 포함될 수 있는 기능이다.

문서가 잘 정리되어 있어서 구현하기 어렵지 않을 거라고 생각했는데, DB 구성이 다르다보니 문서와도 다르게 개발해야 하는 부분이 많아서 예상보다 시간이 많이 소요되었다.

다국어 지원 기능을 개발하면서, DB를 통해 로직을 처리할 때 발생하는 성능적인 부분을 고민하는 시간을 가졌다. 서비스의 주요 기능보다는 부가적인 기능에 가깝기 때문에 최대한 성능과 속도를 중요시해야 했는데 해당 과정에서 캐시, 쿠키, 세션을 공부하고 비교할 수 있었다.

profile
다부진 미래를 만들어가는 개발자
post-custom-banner

0개의 댓글