fos-blog/study
01 / 홈02 / 카테고리
01 / 홈02 / 카테고리

카테고리

  • AI 페이지로 이동
    • RAG 페이지로 이동
    • langgraph 페이지로 이동
    • agents.md
    • BMAD Method — AI 에이전트로 애자일 개발하는 방법론
    • Claude Code의 Skill 시스템 - 개발자를 위한 AI 자동화의 새로운 차원
    • Claude Code를 5주 더 쓴 결과 — 스킬·CLAUDE.md를 키워가는 방식
    • Claude Code를 11일 동안 쓴 결과 — 데이터로 본 나의 사용 패턴
    • Claude Code 멀티 에이전트 — Teams
    • AI 에이전트와 디자인의 새 컨벤션 — DESIGN.md, Google Stitch, Claude Design
    • 하네스 엔지니어링 실전 — 4인 에이전트 팀으로 코딩 파이프라인 구축하기
    • 하네스 엔지니어링 — 오래 실행되는 AI 에이전트를 위한 설계
    • 멀티모달 LLM (Multimodal Large Language Model)
    • AI 에이전트와 함께 MVP 만들기 — dooray-cli 사례
  • ai 페이지로 이동
    • agent 페이지로 이동
  • algorithm 페이지로 이동
    • live-coding 페이지로 이동
    • 분산 계산을 위한 알고리즘
  • architecture 페이지로 이동
    • [초안] 시니어 백엔드를 위한 API 설계 실전 스터디 팩 — REST · 멱등성 · 페이지네이션 · 버전 전략
    • [초안] API Versioning과 Backward Compatibility: 시니어 백엔드 관점 정리
    • 캐시 설계 전략 총정리
    • [초안] CJ푸드빌 커머스/F&B 도메인 설계 면접 대비 — 슬롯 경험을 주문·결제·쿠폰·매장 상태 설계로 번역하기
    • [초안] 커머스 Spring 서비스에 Clean/Hexagonal Architecture를 실용적으로 적용하기
    • [초안] 커머스 주문 상태와 데이터 정합성 기본기 — CJ푸드빌 면접 대비
    • [초안] 쿠폰/프로모션 동시성과 정합성 기본기 — 선착순·중복 사용 방지·발급/사용/복구
    • [초안] DDD와 도메인 모델링: 시니어 백엔드 관점의 전술/전략 패턴 실전 가이드
    • [초안] Decorator & Chain of Responsibility — 행동을 체인으로 조립하는 두 가지 방식
    • 디자인 패턴
    • [초안] 분산 아키텍처 완전 정복: Java 백엔드 시니어 인터뷰 대비 실전 가이드
    • [초안] 분산 트랜잭션과 Outbox 패턴 — 왜 2PC를 피하고 어떻게 대신할 것인가
    • 분산 트랜잭션
    • [초안] e-Commerce 주문·결제 도메인 모델링: 상태머신, 멱등성, Outbox/Saga 실전 정리
    • [초안] F&B 쿠폰·프로모션·멤버십·포인트 설계
    • [초안] F&B · e-Commerce 디지털 채널 도메인 한 장 정리 — CJ푸드빌 디지털 채널 백엔드 면접 대비
    • [초안] F&B 주문/매장/픽업 상태머신 설계 — CJ푸드빌 디지털 채널 백엔드 관점
    • [초안] F&B 이커머스 결제·환불·정산 운영 가이드
    • [초안] Hexagonal / Clean Architecture를 Spring 백엔드에 적용하기
    • [초안] 대규모 커머스 트래픽 처리 패턴 — 1,600만 고객과 올영세일을 버티는 설계
    • [초안] 레거시 JSP/jQuery 화면과 신규 API가 공존하는 백엔드 운영 전략
    • [초안] MSA 서비스 간 통신: Redis [Cache-Aside](../database/redis/cache-aside.md) × Kafka 이벤트 하이브리드 설계
    • [초안] Observability 입문: 시니어 백엔드가 장애를 탐지하고 대응하는 방식
    • [초안] Outbox / Inbox Pattern 심화 — 분산 메시징의 정합성 문제를 DB 트랜잭션으로 풀어내기
    • [초안] 결제 도메인 멱등성과 트랜잭션 재시도 기본기
    • [초안] 시니어 백엔드를 위한 Resilience 패턴 실전 가이드 — Timeout, Retry, Circuit Breaker, Bulkhead, Backpressure
    • [초안] REST API 버저닝과 모바일 앱 하위 호환성 — CJ푸드빌 디지털 채널 백엔드 관점
    • [초안] Strategy Pattern — 분기문을 없애는 설계, 시니어 백엔드 인터뷰 핵심 패턴
    • [초안] 시니어 백엔드를 위한 시스템 설계 입문 스터디 팩
    • [초안] 템플릿 메서드 패턴 - 백엔드 처리 골격을 강제하는 가장 오래되고 가장 위험한 패턴
    • [초안] 대규모 트래픽 중 무중단 마이그레이션 — Feature Flag + Shadow Mode 실전
  • database 페이지로 이동
    • mysql 페이지로 이동
    • opensearch 페이지로 이동
    • redis 페이지로 이동
    • 김영한의-실전-데이터베이스-설계 페이지로 이동
    • 커넥션 풀 크기는 얼마나 조정해야 할까?
    • 인덱스 - DB 성능 최적화의 핵심
    • [초안] JPA N+1과 커머스 조회 모델: 주문/메뉴/쿠폰 도메인에서 살아남기
    • [초안] MyBatis 기본기 — XML Mapper, resultMap, 동적 SQL, 운영 패턴 정리
    • [초안] MyBatis와 JPA/Hibernate 트레이드오프 — 레거시 백엔드를 다루는 시니어 관점
    • 역정규화 (Denormalization)
    • 데이터 베이스 정규화
  • devops 페이지로 이동
    • docker 페이지로 이동
    • k8s 페이지로 이동
    • k8s-in-action 페이지로 이동
    • observability 페이지로 이동
    • [초안] 커머스/F&B 채널 장애 첫 5분과 관측성 기본기
    • Envoy Proxy
    • [초안] F&B / e-Commerce 운영 장애 대응과 모니터링 — 백엔드 관점 정리
    • Graceful Shutdown
  • finance 페이지로 이동
    • industry-cycle 페이지로 이동
    • investing 페이지로 이동
    • stock-notes 페이지로 이동
  • http 페이지로 이동
    • HTTP Connection Pool
  • interview 페이지로 이동
    • [초안] AI 서비스 팀 경험 기반 시니어 백엔드 면접 질문 뱅크 — Spring Batch RAG / gRPC graceful shutdown / 전략 패턴 / 12일 AI 웹툰 MVP
    • [초안] CJ푸드빌 디지털 채널 Back-end 개발자 직무 분석
    • [초안] CJ푸드빌 디지털 채널 Back-end 면접 답변집 — 슬롯 도메인 경험을 커머스/F&B 설계로 번역하기
    • [초안] F&B / e-Commerce 운영 모니터링과 장애 대응 인터뷰 정리
    • Observability — 면접 답변 프레임
    • [초안] 시니어 Java 백엔드 면접 마스터 플레이북 — 김병태
    • [초안] NSC 슬롯팀 경험 기반 질문 은행 — 도메인 모델링·동시성·성능·AI 협업
  • java 페이지로 이동
    • concurrency 페이지로 이동
    • jdbc 페이지로 이동
    • opentelemetry 페이지로 이동
    • spring 페이지로 이동
    • spring-batch 페이지로 이동
    • 더_자바_코드를_조작하는_다양한_방법 페이지로 이동
    • [초안] Java 동시성 락 정리 — 커머스 메뉴/프로모션 정책 캐시 갱신 관점
    • [초안] JVM 튜닝 실전: 메모리 구조부터 Virtual Threads, GC 튜닝, 프로파일링까지
    • Java의 로깅 환경
    • MDC (Mapped Diagnostic Context)
    • Java StampedLock — 읽기 폭주에도 쓰기가 밀리지 않는 락
    • Virtual Thread와 Project Loom
  • javascript 페이지로 이동
    • typescript 페이지로 이동
    • AbortController
    • Async Iterator와 제너레이터
    • CommonJS와 ECMAScript Modules
    • 제너레이터(Generator)
    • Http Client
    • Node 백엔드 운영 패턴 — Streams 백프레셔, pipe/pipeline, 멱등성 vs 분산 락
    • Node.js
    • npm vs pnpm — 어떤 기준으로 선택했나
    • `setImmediate()`
  • kafka 페이지로 이동
    • [초안] Kafka 기본 개념 — 토픽, 파티션, 오프셋, 복제
    • Kafka를 사용하여 **데이터 정합성**은 어떻게 유지해야 할까?
    • [초안] Kafka 실전 설계: 파티션 전략, 컨슈머 그룹, 전달 보장, 재시도, 순서 보장 트레이드오프
    • 메시지 전송 신뢰성
  • linux 페이지로 이동
    • fsync — 리눅스 파일 동기화 시스템 콜
    • tmux — Terminal Multiplexer
  • network 페이지로 이동
    • L2(스위치)와 L3(라우터)의 역할 차이
    • L4와 VIP(Virtual IP Address)
    • IP Subnet
  • rabbitmq 페이지로 이동
    • [초안] RabbitMQ Basics — 실전 백엔드 관점에서 정리하는 메시지 브로커 기본기
    • [초안] RabbitMQ vs Kafka — 백엔드 메시징 선택 기준과 실전 운영 관점
  • security 페이지로 이동
    • [초안] 시니어 백엔드를 위한 보안 / 인증 스터디 팩 — Spring Security, JWT, OAuth2, OWASP Top 10
  • task 페이지로 이동
    • ai-service-team 페이지로 이동
    • nsc-slot 페이지로 이동
    • sb-dev-team 페이지로 이동
    • the-future-company 페이지로 이동
  • testing 페이지로 이동
    • [초안] 시니어 Java 백엔드를 위한 테스트 전략 완전 정리 — 피라미드부터 TestContainers, 마이크로벤치, Contract까지
  • travel 페이지로 이동
    • 오사카 3박 4일 일정표: 우메다 쇼핑, USJ, 난바·도톤보리, 오사카성
  • web 페이지로 이동
    • [초안] HTTP / Cookie / Session / Token 인증 기본기 — 레거시 JSP와 모바일 API가 공존하는 백엔드 관점
FOS-BLOG · FOOTERall systems normal·v0.1 · 2026.04.27·seoul, kr
Ffos-blog/study

개발 학습 기록을 정리하는 블로그입니다. 공부하면서 기록하고, 기록하면서 다시 배웁니다.

visitors
01site
  • Home↗
  • Posts↗
  • Categories↗
  • About↗
02policy
  • 소개/about
  • 개인정보처리방침/privacy
  • 연락처/contact
03categories
  • AI↗
  • Algorithm↗
  • DB↗
  • DevOps↗
  • Java/Spring↗
  • JS/TS↗
  • React↗
  • Next.js↗
  • System↗
04connect
  • GitHub@jon890↗
  • Source repositoryjon890/fos-study↗
  • RSS feed/rss.xml↗
  • Newsletter매주 1 회 · 한 편의 글→
© 2026 FOS Study. All posts MIT-licensed.
built with·Next.js·Tailwind v4·Geist·Pretendard·oklch
fos-blog/task/13개 로케일 다국어 시스템 — Svelte…
system

13개 로케일 다국어 시스템 — Svelte derived 합성 + 백엔드 캐시 사전 구성

진행 기간: 2023.08 2024.02 스포츠 베팅 플랫폼의 다국어 시스템을 프론트엔드부터 백엔드 캐시까지 설계·구현했다. 글로벌 대응을 위해 13개 로케일을 지원했고, 스포츠 베팅이라는 도메인 특성상 UI 문구뿐 아니라 경기 마켓 이름, 선수 이름 치환, 핸디캡 표기 같은 템플릿 번역까지 필요했다. --- 일반 웹 서비스의 i18n과는 결이 달랐다. -...

2026.04.19·6 min read·52 views

진행 기간: 2023.08 ~ 2024.02

스포츠 베팅 플랫폼의 다국어 시스템을 프론트엔드부터 백엔드 캐시까지 설계·구현했다. 글로벌 대응을 위해 13개 로케일을 지원했고, 스포츠 베팅이라는 도메인 특성상 UI 문구뿐 아니라 경기 마켓 이름, 선수 이름 치환, 핸디캡 표기 같은 템플릿 번역까지 필요했다.


요구사항이 만든 제약

일반 웹 서비스의 i18n과는 결이 달랐다.

  • 13개 로케일을 런타임에 전환 — 페이지 리로드 없이 즉시 반영
  • 두 가지 메시지 소스 — 앱 UI 문구(내부 관리)와 경기 용어(Betradar 같은 외부 스포츠 데이터 공급사 제공)가 별개
  • 템플릿 치환 — "{$competitor1} vs {$competitor2}"처럼 선수/팀 이름을 실시간 경기 데이터와 합성
  • 핫 리로드 — 운영 중 어드민에서 번역을 수정하면 앱 재배포 없이 즉시 반영
  • 로케일 의존 파싱 — 일본어는 괄호가 전각(()이라 마켓 이름 정리 정규식이 다르다

이 조합이면 svelte-i18n 같은 라이브러리 하나로는 부족했다. Svelte derived store 기반으로 직접 구성했다.


전체 구조

plaintext
[어드민] ─ MQ(정적 데이터 리로드) ─▶ [백엔드 캐시 리로드]
                                 │
[DB: 언어 / 외부 벤더 언어 테이블]
                                 │
                        [백엔드: 13 로케일 × N 키 맵 사전 구성]
                                 │
                          GET /api/lang/{locale}
                                 │
                                 ▼
      [프론트: LANG_STORE (writable)] ← init(data)
                 │
                 ▼
  [LanguageService: derived 체인]
      └─▶ 템플릿 치환 derived 여러 개 (선수 이름, 핸디캡, outcome)
           └─▶ 상위 합성 derived (marketName, outcomeName …)
                 │
                 ▼
   [컴포넌트: $message('key') — 언어 변경 시 자동 리렌더]

응답 시점 계산을 사전 계산으로 밀어 넣는다는 방향이 양쪽에 공통이다. 백엔드는 캐시 빌드 시점에, 프론트는 derived 그래프 빌드 시점에 계산을 끝낸다.


프론트엔드 — Svelte derived로 반응형 다국어

언어 데이터 store

두 소스(system/외부 벤더)를 하나의 writable에 묶었다.

ts
// 개념 설명용 의사코드
type LangData = {
  vendor: Record<string, string>,   // Betradar 같은 외부 공급사 용어
  system: Record<string, string>,   // 앱 UI 문구
}
 
export const LANG_STORE = writable<LangData>({ vendor: {}, system: {} })

묶은 이유는 단순하다. 언어 변경은 항상 두 맵을 같이 교체한다. 따로 관리하면 둘 중 하나만 갱신된 중간 상태가 UI에 노출될 여지가 있다.

derived를 "함수를 반환하는 store"로 쓴다

핵심 트릭이다. derived가 값이 아니라 (key) => string 함수를 반환하게 만든다.

ts
// 개념 설명용 의사코드
export const message = derived(
  LANG_STORE,
  $store => (key, ...args) => interpolate($store.system[key] ?? key, ...args)
)

컴포넌트에서 $message('login.title')로 쓴다. store가 바뀌면 message 자체가 재구성되고, 이 함수를 호출하는 모든 컴포넌트가 자동 재평가된다.

이 한 줄이 다국어 시스템의 반응성 전부를 담고 있었다. "언어 데이터 + 호출 시 파라미터"를 derived의 클로저 + 반환 함수로 분리하는 덕분에 언어 변경 1번이 모든 소비처에 전파된다.

템플릿 치환 — derived의 합성

스포츠 베팅 마켓 이름은 "Over {total}", "Handicap {$competitor1} +{hcp}" 같은 템플릿. 로케일이 바뀌면 템플릿이 바뀌고 거기에 실시간 경기 데이터가 합성된다. derived를 여러 단계로 쌓아서 풀었다.

ts
// 개념 설명용 의사코드
export const replaceCompetitors = derived(
  vendorMessage,
  $vendor => (template, match) =>
    template
      .replace('{$competitor1}', $vendor(match.homeId, match.homeName))
      .replace('{$competitor2}', $vendor(match.awayId, match.awayName))
)
 
// 상위 합성
export const marketName = derived(
  [vendorMessage, replaceCompetitors],
  ([$vendor, $replace]) => (key, defaultValue, match) => { /* ... */ }
)

실제로는 이런 합성 derived가 6~7개 있다(marketName, outcomeName, 변형 몇 개, highlight 등). 전부 LANG_STORE에 궁극적으로 의존하니 언어 변경 1번이 그래프 전체를 자동 갱신한다.

인사이트. derived 합성은 "의존 그래프"를 선언적으로 표현한다. 새 치환 함수를 추가해도 그래프 끝에 노드 하나 달면 된다. 명령형이었다면 "A 갱신, 그다음 B 갱신..." 같은 순서 관리 코드가 붙었을 것이다.

미번역 키 감지

치환 후에도 {...} placeholder가 남아 있으면 raw가 화면에 노출된다. 모든 derived 끝에 hasLeftoverPlaceholder 가드를 붙여 남아 있으면 defaultValue로 fallback. 번역 키 누락이나 placeholder 불일치가 있어도 깨진 문자열이 직접 노출되진 않는다.


백엔드 — 응답 시점 계산을 캐시 시점으로

백엔드는 13개 로케일 × 수백 개 키를 요청마다 조립하는 구조에서, 로케일별 Map을 캐시가 유지하는 구조로 바꿨다.

java
// 개념 설명용 의사코드
class LanguageCache extends ReloadableKeyedCache<...> {
    private Map<Locale, Map<String, String>> perLocale = new HashMap<>();
 
    protected List<LangRow> loadFromRepo() {
        List<LangRow> rows = repo.findAll();
        writeLockJob(() -> {
            perLocale.clear();
            for (LangRow r : rows)
                for (Locale loc : Locale.values())
                    perLocale.computeIfAbsent(loc, k -> new HashMap<>())
                             .put(r.getKey(), r.getValue(loc));
        });
        return rows;
    }
 
    public Map<String, String> get(Locale loc) {
        return readLockJob(() -> perLocale.get(loc));
    }
}

요청 시엔 cache.get(locale) 한 줄. 응답 객체가 캐시 안에서 참조로 공유된다. 요청마다 수백 개 키 × 로케일별 분기를 돌려 HashMap을 새로 만들던 로직이 사라졌고 GC 압력이 확 줄었다.

리로드 시 부분 상태가 노출되지 않도록 ReentrantReadWriteLock으로 일관성을 잡았다. ConcurrentMap만으로는 "clear + 여러 put"의 스냅샷 일관성이 보장되지 않는다 — 캐시 아키텍처의 동시성 섹션에 같은 패턴을 더 자세히 풀어뒀다.

외부 벤더 메시지(Betradar 등)는 데이터 소스와 업데이트 주기가 달라서 별도 캐시로 분리했다. 같은 캐시에 묶으면 한쪽 변경에 다른 쪽까지 불필요하게 리로드된다.


삽질 포인트

외부 라이브러리와의 키 충돌. 특정 키 네임스페이스가 라이브러리 내부 예약어와 겹쳐 번역이 엉뚱하게 뜨는 hotfix를 한 번 쳤다. 근본 수정은 앱 키에 prefix를 박아 공간 자체를 분리한 것. 외부 라이브러리와 번역 키 공간을 공유하면 언제든 터진다.

일본어만 전각 괄호. 마켓 이름 정리 정규식이 (, )를 타겟으로 했는데, 일본어 마켓 이름은 전각 (, )로 들어왔다. 한 정규식으로 다 처리하려다 실패했고 로케일 체크 후 분기했다. "i18n은 문자열 치환이 아니라 로케일별 파싱 규칙"이라는 걸 실감한 지점이다. 숫자 구분자, 날짜 포맷, 괄호 — 로케일마다 다 다르다.


협업

이 시스템은 프론트·백엔드 양쪽을 내가 작업한 드문 케이스였다. 덕분에 번역 키 네임스페이스를 앱 → 번역팀 → DB → 캐시 → derived까지 한 사람이 설계할 수 있었다. 결정적이었던 건 번역팀과의 계약이었다 — "키는 앱이 정의, 값은 번역팀이 운영"이라는 경계를 먼저 세웠고, 위치 파악이 쉬운 네이밍(home.header.title 같은 점 구분 계층)을 정해서 넘겼다. 이 네이밍이 나중에 키 충돌 이슈를 prefix로 풀 때 기반이 됐다.

어드민팀과의 계약은 "어떤 테이블이 바뀌었다"를 한 필드로 전달하는 것. 리스트 UI 변경과 저장 트리거만 어드민이 담당하고, 캐시 내부는 내가 맡았다. 이 계약을 MQ 단계에서 단순히 유지한 덕에 캐시 종류가 늘어나도 어드민은 건드리지 않아도 됐다.

PR 리뷰에서는 치환 파이프라인 의존 그래프를 직접 그려 올렸다. derived가 어떻게 합성되는지 코드만으로는 파악이 어려워서, 화살표 다이어그램으로 "이 derived는 무엇에 의존하는가"를 한 장으로 보여줬다.


지금 보면

Svelte 5의 rune($derived)을 쓰면 합성이 더 깔끔해진다는 건 명확하다. 더 의미 있는 회고는 다른 지점에 있다.

번역 키 관리를 코드에서 완전히 분리하지 못한 것. 당시엔 "키는 앱이 정의"라고 선을 그었지만 결과적으로 코드에 상수 문자열로 키가 박혔다. 번역 키를 타입 시스템으로 뽑아내서 "존재하지 않는 키를 참조하면 컴파일 에러"가 되도록 했다면, 키 누락 버그가 prod에 올라가는 경로 자체가 닫혔을 것이다. 다음에 같은 문제를 풀면 빌드 타임에 키를 검증하는 파이프라인을 먼저 세울 것 같다.

외부 벤더 캐시의 데이터 동기화 전략. 외부 공급사 데이터 업데이트 주기를 깊게 파악하지 않고 "다르니까 분리"까지만 했다. 실제로는 공급사 업데이트 이벤트를 받는 웹훅이나 스케줄 기반 폴링 중 어느 쪽이 맞는지를 운영 중에 자꾸 조정했다. 설계 단계에서 공급사 API 계약을 더 파고들었으면 이 흔들림을 줄일 수 있었다.


관련 문서

  • Ehcache 캐시 설계 — 같은 리로드 캐시 기반 + MQ 전파 구조
on this page
  • 01요구사항이 만든 제약
  • 02전체 구조
  • 03프론트엔드 — Svelte derived로 반응형 다국어
  • 언어 데이터 store
  • derived를 "함수를 반환하는 store"로 쓴다
  • 템플릿 치환 — derived의 합성
  • 미번역 키 감지
  • 04백엔드 — 응답 시점 계산을 캐시 시점으로
  • 05삽질 포인트
  • 06협업
  • 07지금 보면
  • 08관련 문서

댓글 (0)