글로벌 서비스를 위한 다국어 데이터베이스 설계하기

Designing a Database for Localized Content
글로벌 서비스를 제공하려면 다국어 데이터를 효율적으로 저장하고 관리할 수 있는 데이터베이스 설계가 필수적입니다. 단순히 번역된 문자열을 저장하는 것을 넘어, 지역별 콘텐츠 최적화, 성능 고려, 확장성까지 신경 써야 합니다. 다국어 데이터베이스 설계 방식에는 별도 테이블 사용, 컬럼 확장, JSON 저장, Key-Value 스토어 활용 등 다양한 접근법이 있으며, 각각의 장단점과 적절한 사용 사례를 이해하는 것이 중요합니다.
이 글에서는 국제화(i18n)와 현지화(l10n)를 고려한 데이터베이스 설계 전략을 분석하고, 이를 실무에 적용하는 방안을 살펴봅니다.
Various Localization Strategies
다국어 지원이 필요한 시스템을 설계할 때, 데이터베이스에서 다양한 전략을 사용할 수 있습니다. 각 전략은 성능, 확장성, 유지보수성 측면에서 차이가 있으며, 사용 사례에 따라 적절한 방식을 선택하는 것이 중요합니다. 여기서는 대표적인 세 가지 전략에 대해 살펴보겠습니다.
1. Column Expansion Strategy
컬럼 확장 전략은 다국어 데이터를 저장하는 가장 단순한 방법 중 하나로, 테이블에 각 언어별 컬럼을 추가하여 데이터를 저장하는 방식입니다. 예를 들어, 제품명을 저장하는 product_name
필드가 있다면, 다국어 지원을 위해 product_name_ko
, product_name_en
, product_name_ja
와 같이 각 언어별 컬럼을 추가하는 방식입니다.
이 전략은 구현이 직관적이고 간단하여 빠르게 적용할 수 있습니다. 언어별로 별도 컬럼을 추가하는 방식이므로 SQL 쿼리가 단순합니다. 그러나 지원해야 하는 언어가 많아질 경우 테이블 구조가 지나치게 커지고 성능이 저하될 수 있습니다. 또한 새로운 언어를 추가할 때마다 컬럼을 추가해야 하므로 확장성이 제한됩니다.
1-1. Strengths and Weaknesses of Column Expansion Strategy
컬럼 확장 전략은 초기 지원 언어가 적을 때 적용이 간편하고 특정 언어의 데이터를 빠르게 조회할 수 있다는 장점이 있습니다. 별도의 조인 없이 단일 테이블에서 데이터를 가져올 수 있어 성능 면에서도 유리합니다. 하지만 언어가 추가될 때마다 스키마를 변경해야 하므로 확장성이 낮고, 컬럼 수가 많아질수록 테이블이 비대해져 관리가 복잡해질 수 있습니다. 또한, 컬럼 수 증가로 인해 인덱스 효율이 떨어지고, 특정 데이터베이스에서는 불필요한 컬럼을 포함한 스캔 비용이 증가하여 조회 성능이 저하될 가능성이 있습니다.
- 빠른 데이터 조회: 언어별 데이터가 같은 테이블 내에 존재하므로, 별도의 조인 없이 빠르게 조회할 수 있습니다.
- 쉬운 SQL 작성: 다국어 데이터를 조회할 때 복잡한 조인이 필요하지 않으므로, 유지보수가 용이합니다.
- 적용이 용이: 기존 시스템에 쉽게 적용할 수 있어 개발 부담이 적습니다.
하지만, 몇 가지 단점도 고려해야 합니다.
- 확장성이 부족함: 새로운 언어가 추가될 때마다 테이블 구조를 변경해야 하므로, 장기적으로 유지보수가 어렵습니다. 또한, 지원하는 언어가 많아질수록 테이블의 크기가 증가하여 스토리지 사용량이 증가하고, 테이블 조회 성능이 저하될 가능성이 있습니다.
- 컬럼 증가 문제: 지원하는 언어가 많아질수록 테이블에 컬럼이 계속 추가되며, 데이터 구조가 복잡해질 수 있습니다.
- NULL 값 증가 가능성: 일부 언어에 대한 번역이 제공되지 않을 경우, 해당 컬럼이 NULL 값으로 채워질 가능성이 큽니다.
1-2. Table Schema for Column Expansion Strategy
컬럼 확장 전략에서는 각 언어별로 개별 컬럼을 생성하여 번역된 데이터를 저장합니다. 다음은 이러한 전략을 적용한 테이블 구조 예제입니다.
create table products
(
id bigint auto_increment primary key,
product_name_ko varchar(255), -- 한국어 제품명
product_name_en varchar(255), -- 영어 제품명
product_name_ja varchar(255), -- 일본어 제품명
description_ko varchar(1000), -- 한국어 제품 설명
description_en varchar(1000), -- 영어 제품 설명
description_ja varchar(1000), -- 일본어 제품 설명
price decimal(10, 2)
);
위 테이블은 각 언어별로 제품명과 설명을 별도 컬럼에 저장하여 관리합니다. 새로운 제품을 추가할 때는 각 언어의 값을 함께 저장해야 합니다.
insert into products (product_name_ko, product_name_en, product_name_ja, description_ko, description_en, description_ja, price)
values ('스마트폰', 'Smartphone', 'スマートフォン', '최신 기술이 적용된 고급 스마트폰입니다.', 'A high-end smartphone with advanced features.', '最新技術が搭載された高級スマートフォンです。', 999.99);
이제 저장된 데이터를 조회하는 쿼리를 살펴보겠습니다. 특정 언어로 제공되는 제품명과 설명을 가져오려면 다음과 같은 쿼리를 실행할 수 있습니다.
select
id as `Product ID`,
product_name_en as `Product Name`,
products.description_en as `Product Description`
from
products
where
id = 1;
실행 결과는 다음과 같습니다:
Product ID | Product Name | Product Description |
---|---|---|
1 | Smartphone | A high-end smartphone with advanced features. |
이처럼 컬럼 확장 전략을 사용하면 특정 언어의 데이터를 별도의 조인 없이 간편하게 조회할 수 있습니다. 하지만 언어가 추가될 때마다 테이블 스키마를 변경해야 하는 단점이 있을 수 있습니다.
2. Column-Based Translation Table Strategy
컬럼 기반 번역 테이블 전략은 다국어 데이터를 개별 테이블 컬럼이 아니라, 별도의 번역 테이블에서 관리하는 방식입니다. 이 방식에서는 제품명이나 설명과 같은 번역 가능한 필드를 원본 테이블에 직접 저장하지 않고, 번역 테이블을 참조하여 필요한 언어의 데이터를 가져옵니다.
2-1. Strengths and Weaknesses of Column-Based Translation Table Strategy
이 전략은 기존 데이터 모델을 크게 변경하지 않고 다국어 지원을 추가할 수 있는 장점이 있습니다. 중앙 번역 테이블을 활용함으로써 중복 저장을 최소화할 수 있으며, 새로운 언어가 추가될 때 테이블 구조 변경 없이 데이터만 추가하면 되므로 확장성이 뛰어납니다. 다만, 조회 시 원본 테이블과 번역 테이블 간의 조인이 필요하여 성능 저하 가능성이 있으며, SQL이 복잡해질 수 있습니다.
- 기존 모델 변경 최소화: 새로운 언어를 추가할 때 테이블 구조를 변경할 필요 없이 데이터만 삽입하면 됩니다.
- 번역 데이터 재사용 가능: 동일한 번역이 여러 테이블에서 사용될 경우, 하나의 번역 데이터를 참조하여 관리할 수 있습니다.
- 데이터 일관성 유지: 번역된 데이터를 중앙에서 관리하므로, 동일한 텍스트가 여러 곳에서 일관되게 유지됩니다.
하지만, 몇 가지 단점도 고려해야 합니다.
- SQL 복잡성 증가: 다국어 데이터를 조회하려면 번역 테이블과 조인을 해야 하므로, SQL 쿼리가 상대적으로 복잡해질 수 있습니다. 특히, 각 번역 필드(예: 제품명, 설명 등)에 대해 개별적으로 조인을 수행해야 하므로, 조회 성능과 쿼리 가독성이 영향을 받을 수 있습니다.
- NULL 값 문제: 특정 언어 번역이 제공되지 않는 경우, 번역 테이블에서 해당 컬럼이 NULL이 될 수 있습니다.
2-2. Table Schema for Column-Based Translation Table Strategy
컬럼 기반 번역 테이블 전략에서는 번역 데이터를 별도의 테이블에 저장하고, 원본 테이블에서는 해당 번역의 참조 ID만 유지합니다. 이를 통해 다국어 데이터를 효율적으로 관리할 수 있습니다. 다음은 이를 적용한 테이블 구조 예제입니다.
create table translations
(
id bigint auto_increment primary key,
text_ko varchar(1000), -- 한국어 번역 텍스트
text_en varchar(1000), -- 영어 번역 텍스트
text_ja varchar(1000) -- 일본어 번역 텍스트
);
create table products
(
id bigint auto_increment primary key,
category_id bigint not null,
price decimal(10, 2) not null,
image_url varchar(255) not null,
name_translation_id bigint not null,
description_translation_id bigint not null,
foreign key (name_translation_id) references translations (id),
foreign key (description_translation_id) references translations (id)
);
위 구조에서는 products
테이블에서 직접 다국어 데이터를 저장하지 않고, translations
테이블을 참조하여 번역된 텍스트를 관리합니다.
-- 번역된 텍스트 추가 (한 레코드에 모든 언어 번역 포함)
insert into
translations (text_ko, text_en, text_ja)
values
('스마트폰', 'Smartphone', 'スマートフォン'),
('최신 기술이 적용된 고급 스마트폰입니다.', 'A high-end smartphone with advanced features.', '最新技術が搭載された高級スマートフォンです。');
-- products 테이블에 번역 ID 설정하여 데이터 삽입
insert into
products (category_id, price, image_url, name_translation_id, description_translation_id)
values
(1, 999.99, 'image.jpg', 1, 2);
이제 저장된 데이터를 조회하는 예제를 살펴보겠습니다. 일본어로 제공되는 제품명과 설명을 가져오려면 다음과 같은 쿼리를 실행할 수 있습니다.
select
p.id as `Product ID`,
productName.text_ja as `Product Name`,
productDesc.text_ja as `Product Description`
from
products p
left join translations productName on p.name_translation_id = productName.id
left join translations productDesc on p.description_translation_id = productDesc.id
where
p.id = 1;
실행 결과는 다음과 같습니다:
Product ID | Product Name | Product Description |
---|---|---|
1 | スマートフォン | 最新技術が搭載された高級スマートフォンです。 |
이 방식은 번역 데이터를 별도의 테이블에서 관리하므로 스키마 변경 없이 새로운 언어를 추가할 수 있습니다.
3. Row-Based Translation Table Strategy
로우 기반 번역 테이블 전략은 다국어 데이터를 개별 컬럼이 아닌, 개별 행(Row)으로 저장하는 방식입니다. 이 방식에서는 번역 가능한 필드를 원본 테이블에서 직접 저장하지 않고, 번역 테이블에서 로케일(Locale) 값을 기준으로 특정 언어 데이터를 조회할 수 있습니다.
3-1. Strengths and Weaknesses of Row-Based Translation Table Strategy
이 전략은 새로운 언어를 추가할 때 테이블 구조 변경 없이 데이터를 삽입할 수 있는 높은 확장성을 제공합니다. 또한, 다국어 데이터를 조회할 때 로케일 조건을 활용하면 조인을 최소화할 수 있어 성능이 향상될 수 있습니다.
- 확장성 높음: 새로운 언어가 추가될 때 테이블 구조를 변경할 필요 없이 데이터만 삽입하면 됩니다.
- 필요한 언어만 조회 가능: 로케일 값을 조건으로 지정하면 특정 언어만 조회할 수 있어 성능 최적화가 가능합니다.
- 중복 저장 최소화: 동일한 번역 데이터를 여러 테이블에서 공유할 수 있어 데이터 일관성을 유지할 수 있습니다.
하지만, 몇 가지 단점도 고려해야 합니다.
- 조회 시 조인 필요: 로케일 조건을 활용하여 조회해야 하므로, 원본 테이블과 번역 테이블 간의 조인이 필요합니다.
- 전체 데이터 조회 시 성능 부담: 특정 언어만 조회할 때는 효율적이지만, 전체 데이터를 조회할 경우 로우 수가 많아질 수 있어 성능 최적화가 필요합니다.
3-2. Table Schema for Row-Based Translation Table Strategy
로우 기반 번역 테이블 전략에서는 원본 테이블에서 번역이 필요한 컬럼을 전용 번역 테이블로 분리하여 저장하며, 원본 테이블에는 번역이 필요 없는 정보만 유지합니다. 이를 통해 데이터 조회 시 번역 테이블을 참조하여 원하는 언어의 데이터를 가져올 수 있습니다. 번역 테이블은 로케일 값을 기준으로 각 언어별 데이터를 개별 행으로 저장하며, 하나의 테이블에서 여러 언어의 번역 데이터를 효과적으로 관리할 수 있습니다. 다음은 이 전략을 적용한 테이블 구조 예제입니다.
create table products
(
id bigint auto_increment primary key,
category_id bigint not null,
price decimal(10, 2) not null,
image_url varchar(255) not null
);
create table product_translations
(
id bigint auto_increment primary key,
product_id bigint not null,
locale varchar(10) not null, -- 언어 코드 (ko, en, ja 등)
name varchar(255) not null, -- 번역된 제품명
description text not null, -- 번역된 제품 설명
unique key unique_translation (product_id, locale),
foreign key (product_id) references products (id) on delete cascade
);
제품 정보에서 번역이 필요한 데이터는 이제 전용 번역 테이블에서 로케일별로 관리됩니다. 이를 통해 다국어 데이터를 체계적으로 저장할 수 있으며, 새로운 언어를 추가해도 테이블 구조를 변경할 필요가 없습니다.
-- 원본 제품 데이터 추가
insert into
products (category_id, price, image_url)
values
(1, 999.99, 'image.jpg');
-- 번역된 텍스트 추가 (각 언어별로 개별 행으로 저장)
insert into
product_translations (product_id, locale, name, description)
values
(1, 'ko', '스마트폰', '최신 기술이 적용된 고급 스마트폰입니다.'),
(1, 'en', 'Smartphone', 'A high-end smartphone with advanced features.'),
(1, 'ja', 'スマートフォン', '最新技術が搭載された高級スマートフォンです。');
이제 저장된 데이터를 조회하는 방법을 살펴보겠습니다. 영어로 번역된 제품명과 설명을 가져오려면 다음과 같은 쿼리를 실행하면 됩니다:
select
product_id as `Product ID`,
name as `Product Name`,
description as `Product Description`
from
product_translations
where
product_id = 1
and locale = 'en';
쿼리를 실행하면 다음과 같은 결과를 확인할 수 있습니다. 각 행은 지정된 로케일에 맞는 번역 데이터를 포함하고 있으며, 이를 통해 다국어 지원이 원활하게 이루어집니다.
Product ID | Product Name | Product Description |
---|---|---|
1 | Smartphone | A high-end smartphone with advanced features. |
이처럼 로우 기반 번역 테이블 전략을 사용하면 다국어 데이터를 효과적으로 관리할 수 있으며, 새로운 언어를 추가할 때도 기존 테이블 구조를 변경할 필요가 없습니다.
Implementation of a Localized Database
지금까지 특정 지역과 문화에 맞게 번역된 콘텐츠를 관리하는 세 가지 전략에 대해 살펴보았습니다. 개인적으로는 확장성과 유지보수성을 고려할 때, 로우 기반 번역 테이블(Row-Based Translation Table) 전략이 균형적인 선택이라고 생각합니다. 이 방식은 새로운 언어가 추가되더라도 구조를 변경할 필요가 없으며, 적절한 수준의 성능을 보장할 것으로 보입니다. 이제 실제 코드로 이 전략을 구현해 보면서 구체적인 동작 방식을 살펴보겠습니다.
1. Database Schema Definition
먼저, 다국어 데이터를 저장하기 위한 테이블을 정의합니다. products
테이블에는 제품의 기본 정보가 포함되며, product_translations
테이블에는 각 제품의 번역된 텍스트가 저장됩니다. 두 테이블은 product_id
를 통해 연결되며, 특정 언어를 조회할 때 locale
을 기준으로 필터링됩니다.
create table if not exists products
(
id bigint auto_increment primary key,
category_id bigint not null,
price decimal(10, 2) not null,
image_url varchar(255) not null
);
create table if not exists product_translations
(
id bigint auto_increment primary key,
product_id bigint not null,
locale varchar(10) not null,
name varchar(255) not null,
description text not null,
unique key unique_translation (product_id, locale),
foreign key (product_id) references products (id) on delete cascade
);
2. Insert Sample Data
다음으로, 실제 데이터가 어떻게 저장되는지 확인하기 위해 더미 데이터를 삽입합니다. 먼저 products
테이블에 제품 정보를 추가하고, 이후 각 제품에 대한 번역된 정보를 product_translations
테이블에 저장합니다.
insert into
products (category_id, price, image_url)
values
(10, 999.99, 'iphone.jpg');
insert into
product_translations (product_id, locale, name, description)
values
(1, 'en', 'iPhone 15', 'The latest iPhone model'),
(1, 'ko', '아이폰 15', '최신 아이폰 모델'),
(1, 'ja', 'アイフォン15', '最新のアイフォンモデル');
위와 같은 방식으로 각 언어별 번역 데이터를 개별 행으로 저장할 수 있으며, 필요에 따라 새로운 언어 데이터를 쉽게 추가할 수 있습니다.
3. API Implementation
이제 다국어 데이터를 제공하는 API를 구현해 보겠습니다. Spring Boot 환경에서 컨트롤러를 작성하여 특정 제품 정보를 요청하면 번역된 데이터를 포함한 결과를 반환하도록 합니다.
@GetMapping("/products/{id}")
public ResponseEntity<?> products(
@PathVariable Long id
) {
ProductResult result = service.retrieve(id);
return ResponseEntity
.ok(result);
}
다국어 데이터를 적절하게 제공하기 위해서는 요청자의 로케일 정보를 알아야 합니다. 이때, Spring i18n을 활용하면 로케일 정보를 자동으로 감지할 수 있습니다. 클라이언트가 요청 헤더에 Accept-Language
값을 전달하면, Spring은 이를 기준으로 로케일을 추출하고 전역 컨텍스트 저장소에 보관하여 스레드 단위로 해당 정보를 활용할 수 있게 됩니다. 이를 통해 애플리케이션은 클라이언트의 언어 환경에 맞춘 데이터를 제공할 수 있으며, 별도의 추가 설정 없이도 로케일에 따른 적절한 국제화 처리가 이루어집니다.
public ProductResult retrieve(Long id) {
Locale locale = LocaleContextHolder.getLocale();
return productRepository.findProductWithTranslationById(id, locale);
}
이 로케일을 기준으로 특정 언어에 해당하는 데이터를 조회하는 쿼리를 아래와 같이 작성할 수 있습니다:
@Override
public ProductResult findProductWithTranslationById(Long id, Locale locale) {
return queryFactory
.select(Projections.constructor(
ProductResult.class,
product.id,
product.price,
product.imageUrl,
productTranslation.name,
productTranslation.description
))
.from(product)
.innerJoin(productTranslation).on(productTranslation.product.eq(product))
.where(
product.id.eq(id),
productTranslation.locale.eq(locale.getLanguage())
)
.fetchOne();
}
4. API Request and Response
다국어 지원 테스트를 위한 간단한 API가 구현되었습니다. 이를 통해 다국어 데이터를 실제로 조회해 보겠습니다. 클라이언트는 Accept-Language
헤더를 설정하여 원하는 언어로 데이터를 요청할 수 있으며, 서버는 해당 언어에 맞는 번역 데이터를 반환합니다. 이를 통해 하나의 API 엔드포인트로 여러 언어를 지원할 수 있습니다.
영어 데이터를 조회하려면 Accept-Language
헤더에 en
값을 설정하여 요청을 보냅니다.
GET /products/1 HTTP/1.1
Content-Type: application/json
Accept-Language: en
Host: localhost:8080
이 요청이 처리되면, 서버는 영어 버전의 데이터를 조회하여 클라이언트에 반환합니다.
{
"id": 1,
"price": 999.99,
"imageUrl": "iphone.jpg",
"name": "iPhone 15",
"description": "The latest iPhone model"
}
이번에는 Accept-Language
값을 ja
로 설정하여 일본어 데이터를 요청해 보겠습니다.
GET /products/1 HTTP/1.1
Content-Type: application/json
Accept-Language: ja
Host: localhost:8080
이 경우, API가 정상적으로 동작하면 일본어 버전의 데이터가 반환됩니다.
{
"id": 1,
"price": 999.99,
"imageUrl": "iphone.jpg",
"name": "アイフォン15",
"description": "最新のアイフォンモデル"
}
이와 같은 방식으로 클라이언트의 로케일에 따라 다양한 언어로 데이터를 제공할 수 있습니다. 특히, 새로운 언어가 추가되더라도 테이블 구조를 변경하지 않고 데이터만 삽입하면 되므로, 시스템의 유지보수가 용이하고, 향후 새로운 언어를 지원하는 데 필요한 노력도 최소화됩니다.
Final Thoughts
사이드 프로젝트를 구상하면서 글로벌 시장을 노려볼까 하니, 자연스럽게 다국어 지원에 대한 고민이 생겼습니다. 처음에는 가볍게 시작한 사이드 프로젝트인데 복잡한 다국어 지원을 피하고 영어 기반으로만 제공할까 했으나, 최소한 한국어까지는 지원해야 하지 않을까 하는 그런 의무감이 들었습니다. 🫠 이에 따라 다국어 데이터를 효율적으로 관리하는 다양한 전략과 데이터베이스 설계 방식에 대해 알아보게 되었습니다.
여기서 다룬 전략 외에도 JSON을 활용한 스키마리스 데이터 저장 방식이나 Key-Value 스토어를 이용하는 방법, 각 언어별로 별도의 테이블을 운영하는 전략, 그리고 외부 번역 서비스와 연동하는 방식 등 다양한 접근법을 찾을 수 있었습니다. 프로젝트의 규모, 지원해야 할 언어 수, 데이터 변경의 빈도, 성능 요구사항 등을 종합적으로 고려한 후, 적절한 전략을 선택해야 할 것 같습니다. 🤔💭
이 글에서 사용된 구현 코드는 🐙 GitHub 리포지토리에서 확인할 수 있습니다. 🚀
- Spring
- Architecture
- Database