한영 변환 기능 관련 얘기가 나왔다. 내가 하게 되는지는 확정되지 않았지만...
예전에 타임리프 태그로는 언어 변환을 해본 적이 있다. 반면 JSP에서는 해본 적이 없어서 이번 기회에 정리해보려고 한다. 이번에는 단순히 한/영만 고려하지 않고, 확장성을 위해 다국어로 생각해봤다.
일단 간단하게 떠올려본 방식은 아래 네 가지다.
1. properties에 정리하고 JSTL `<fmt:message>` 태그로 변환 (예: `message_ko.properties`, `message_en.properties`)
2. 언어별 별도 데이터베이스 사용 (예: `mariadb://../project1_kr`, `mariadb://../project1_en`)
3. 언어별 별도 테이블 분리 (예: `project.table1_kr`, `project.table1_en`)
4. 언어별 컬럼 분리 (예: `table.column_kr`, `table.column_en`)
그리고 추가로 찾아본 방법은 아래 네 가지였다.
5. JSP 내에서 직접 언어 분기 (`<% if ("en".equals(lang)) ... %>`)
6. 백엔드에서 request에 메시지를 설정 (`request.setAttribute("message", message)`)
7. JavaScript로 언어 분기 처리
8. `<spring:message>` 태그 사용 (1번과 유사)
5. JSP 내에서 직접 언어 분기
6. 백엔드에서 request에 메시지를 설정
7. JavaScript로 언어 분기 처리
5~7번 방식은 프론트든 백엔드든 코드 내에 언어 분기 로직을 직접 넣어야 하기 때문에, 페이지 수가 많아질수록 유지보수 부담이 커지고 분기처리도 많아진다. 텍스트가 간단한 것만 있다면 할만한 방법이다.
1. properties에 정리하고 JSTL
8. `<spring:message>` 태그 사용
1번과 8번은 구조가 단순하고 유지보수가 쉬운 편이지만, 사용자 입력처럼 동적으로 생성되는 콘텐츠는 처리하기 어렵고, 수정할 때마다 재배포가 필요하다는 단점이 있지만, 컨텐츠가 정해져 있는 경우에는 이렇게 처리하는게 가장 좋아보인다.
2. 언어별 별도 데이터베이스 사용 (예: `mariadb://../project1_kr`, `mariadb://../project1_en`)
3. 언어별 별도 테이블 분리 (예: `project.table1_kr`, `project.table1_en`)
4. 언어별 컬럼 분리 (예: `table.column_kr`, `table.column_en`)
2~4번은 DB 관리 방식이기 때문에 위 방식들에 비해 상대적으로 유연하고, 관리 측면에서도 나은 선택이라고 생각한다.
2번은 데이터베이스 자체를 바꿔야 하므로, “하나의 프로젝트인데 굳이 DB를 나눠야 할까?” 하는 고민이 든다. 특히 다국어 DB를 항상 동일하게 유지해야 하므로 관리 비용도 발생한다.
3번과 4번은 언어 파라미터에 따라 테이블이나 컬럼을 동적으로 선택하는 방식이다. 구현 방식만 잘 잡는다면 다른 방법들에 비해 가장 무난한 선택 같다.
나의 경우, 컬럼으로 구분한 방식은 사용해본 적이 있는데, 전체 데이터를 내려주고 프론트에서 해당 언어 컬럼을 선택해서 보여주는 구조였다. 이건 프론트와 백엔드 중 어디에서 언어 처리 책임을 둘지에 따라 달라질 수 있을 것 같다.
아무튼 서론이 길었는데, 개인적으로 가장 현실성 있어 보이는 3번과 4번 방식만 정리해보려고하다가, 모든 글자를 DB 에 저장해서 사용한다면 관리가 끔찍할 것 같아서, "1 또는 8" + "3 또는 4" 형식이 좋아보였다. 메뉴명 같은 경우는 웬만해서는 바뀌지 않기 때문에 properties에 정의하고 공지사항 같은 동적 컨텐츠는 관리자가 언제든지 발행하고 수정할 수 있어야하기 때문에 DB에 저장하는 걸로 생각했다.
1. 정적 컨텐츠 다국어 처리


먼저 기본 값에 대해서 `<spring:message>` 를 사용한 다국어처리를 했다. `<fmt:message>` 의 경우 properties 파일을 기본 ISO 인코딩으로 불러온다고 해서 한국어가 깨지는 문제가 발생했다. 스프링 프레임워크에는 spring message가 권장되는 방식이라서 해당 태그를 사용했다.


언어 변경을 AWS 를 보면 URL로 언어를 구분하고 있는데, 구성 자체가 다른걸 볼 수가 있다. 내가하려는건 구조는 동일하되 정적 컨텐츠는 언어만 바꾸고, 동적 컨텐츠는 게시물을 별도로 관리하게끔 하는 거라서 세션기반을 사용했다.
또한 지속성이 중요하다고 판단하지 않아서 세션으로 사용했는데, 자주 방문하는 페이지이고 사용자가 많다면 쿠키로 하는게 좋을 것 같긴하다.
@Controller
public class LocaleController {
@PostMapping("/change-lang")
public String changeLang(@RequestParam("lang") String lang,
HttpServletRequest request,
HttpServletResponse response,
HttpSession session,
@RequestHeader("Referer") String referer) {
session.setAttribute("lang", lang);
return "redirect:" + referer;
}
}
`select`에서 변경할 때마다 언어변경 API 를 호출해서 세션에 저장한다.
@Component
public class LocaleFilter implements Filter {
@Autowired
private LocaleResolver localeResolver;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
HttpSession session = req.getSession(false);
if (session != null) {
String lang = (String) session.getAttribute("lang");
if (lang != null) {
Locale locale = new Locale(lang);
localeResolver.setLocale(req, res, locale);
}
}
chain.doFilter(request, response);
}
}
`LocaleResolver` 는 언어 타입에 맞는 message 를 출력하도록 도와준다. 세션에 설정된 lang 이 있으면 `LocaleResolver`에 해당 언어를 등록해서 유지되도록 해준다.
2. 테이블 분리

현재는 언어 변환만 하는 것이기 때문에 기본 테이블인 `board` 만 조회하고 있다. 언어별로 테이블을 생성해서 해당 테이블에 작성/조회/업데이트/삭제를 진행할 수 있도록 해봤다.
public List<Board> getBoardList(HttpSession session) {
List<Board> boards = boardMapper.selectBoards(new HashMap<String, Object>() {{
put("table", getBoardTable(session));
}});
return boards;
}
private String getBoardTable(HttpSession session) {
String lang = (String) session.getAttribute("lang");
if (lang == null || lang.equals("ko")) {
return "board";
} else {
return "board_" + lang;
}
}
<select id="selectBoards" resultType="Board">
SELECT id, writer, title, created_date
FROM ${table}
ORDER BY id DESC
</select>
`session`에 있는 언어를 가져와서 테이블을 구분한 다음 파라미터로 보내 해당 테이블을 조회할 수 있게 했다.
(동적 SQL 은 SQL 인젝션 위험이 있기 때문에 실무에 적용할 때는 분기처리로 정확한 테이블명을 전달해야한다.)


저장하면 해당하는 언어의 포스트만 보이는 것을 확인할 수 있다.
3. 컬럼 분리

board 테이블에 언어별 컨텐츠 컬럼을 생성한다.
public List<Board> getBoardList(HttpSession session) {
List<Board> boards = boardMapper.selectBoards(new HashMap<String, Object>() {{
put("postfix", getBoardPostfix(session));
}});
return boards;
}
private String getBoardPostfix(HttpSession session) {
String lang = (String) session.getAttribute("lang");
if (lang == null || lang.equals("ko")) {
return "";
} else {
return "_" + lang;
}
}
<select id="selectBoards" resultType="Board">
SELECT id
, writer
, title${postfix} as title
, created_date
FROM board
ORDER BY id DESC
</select>
테이블과 마찬가지라 언어에 따라 컬럼을 선택할 수 있게 `mybatis` 에 동적으로 들어가게 한다.


확인해보면 나머지는 다 같고 언어에 따라 컨텐츠가 다르게 표시되는 걸 볼 수 있다. (작성자는 언어 구분을 하지 않아서 동일)
따로 구현하진 않았지만 이 방법의 경우 언어별로 게시글을 동시에 작성해야하는 구조이기 때문에 그만큼 한번에 작성해야하는 양과 파라미터가 많아진다. 프론트와 백엔드 둘 다 관리하기 어려워지는 구조인 것 같다.
하고나니 URL 기반으로 하면 `UrlLocalResolver` 사용해서 세션 저장할 필요없어서 편해보인다.
테이블, 컬럼 중에는 테이블 분리가 관리까지 편해서 사용하기 좋았다.
'IT' 카테고리의 다른 글
| [Spring Batch] Cursor vs Paging (2) | 2025.07.25 |
|---|---|
| [Spring] @Transactional 이 일어나지 않는 경우 (1) | 2025.07.23 |
| [Cloudflare Tunnel] 우리집 IP 노출 없이 노트북을 연결해보자 (1) | 2025.07.16 |
| [Oracle Cloud] 인스턴스 생성 자동화 (Out of host capacity) (1) | 2025.07.03 |
| [Oracle Cloud] 오라클 클라우드 프리티어(Free Tier) 가입 (0) | 2025.07.02 |