공공데이터 번역 cache와 원문 보존

UI 문구, 검수 번역, 기계 번역, 한국어 fallback을 분리했다

한강자리는 한국어 공공데이터를 여러 언어로 보여줘야 한다. 앱 버튼과 탭 이름만 번역하면 끝나는 일이 아니었다. 사용자가 실제로 보는 것은 행사명, 시설명, 공지, 혼잡 메시지처럼 매번 원천에서 들어오는 텍스트다.

예를 들어 외국어 사용자가 여의도 공원 정보를 본다고 해보자. 버튼은 영어로 보이는데 행사명은 비어 있거나, 시설명이 이상하게 번역되거나, 공지 원문을 찾을 수 없다면 앱 전체가 불안해 보인다. 이 사용자는 번역 파이프라인을 보지 않는다. 화면에 빈칸이 있는지, 원문이라도 자연스럽게 남아 있는지만 본다.

그래서 한강자리에서는 고정 UI 문구와 매번 들어오는 공공데이터 번역을 분리했다. UI 문구는 릴리스와 함께 바뀌지만, 공공데이터 원문은 수집될 때마다 바뀐다. 이 둘을 같은 방식으로 다루면 어느 쪽도 편하지 않았다.

두 종류의 번역

영역원본바뀌는 때저장 위치
앱 UI 문구String Catalog앱 릴리스iOS 리소스
공공데이터 텍스트한국어 원문원천 수집 때Postgres
검수/공식 번역사람이 확인한 파일검수 때JSON 파일
기계 번역한국어 원문 hash수집 뒤 번역 작업번역 캐시

UI 문구는 key가 먼저다. 코드에는 사람이 읽는 말이 아니라 key가 들어가고, 실제 문구는 String Catalog에 둔다.

공공데이터는 반대다. 원문이 먼저 들어온다. 그 원문을 잃으면 출처 확인도, 재번역도, 원문으로 대신 보여주는 일도 어려워진다.

그래서 공공데이터에서 한국어 원문은 단순한 대체 문구가 아니라 나중에 다시 확인할 원본 기록에 가깝다. 번역은 화면에 보여주려고 만든 값이다. 번역이 어색하거나 특정 언어가 비어 있어도, 원문과 출처 정보가 남아 있으면 다시 번역하거나 검수 파일로 덮어쓸 수 있다.

번역은 검수된 값부터 찾는다

한강자리의 공공데이터 번역은 한 출처만 믿지 않는다. 언어별로 우선순위를 두고, 화면에 보여줄 가장 믿을 만한 값을 찾는다.

flowchart TD
  Request["표시할 한국어 원문과 언어"] --> Official{"공식 번역 있음?"}
  Official -->|예| UseOfficial["공식 번역 표시"]
  Official -->|아니오| Curated{"검수/수정 번역 있음?"}
  Curated -->|예| UseCurated["검수 번역 표시"]
  Curated -->|아니오| Machine{"캐시에<br/>해당 언어 있음?"}
  Machine -->|예| UseMachine["캐시 번역 표시"]
  Machine -->|아니오| Korean["한국어 원문 표시"]

이 순서는 앱을 믿게 하는 일과 연결된다. 공식 또는 검수된 번역이 있으면 기계 번역보다 우선한다. 기계 번역이 없거나 위험한 번역이면 한국어 원문으로 돌아간다.

빈 문자열이나 key는 화면에 보여주지 않는다. 한국어 원문으로 대신 보여주더라도 사용자에게는 의도된 표시처럼 보여야 한다.

번역 캐시는 빠진 언어만 채운다

기계 번역 캐시는 한국어 원문 hash를 key로 한다. 같은 문자열이 행사, 시설, 주차장, 공원에서 반복되면 한 번만 번역하면 된다.

캐시 한 줄에는 원문, 언어별 번역값, 번역 엔진, 성공 여부, 만든/고친 시각이 들어간다. 여기서 봐야 하는 것은 “이 줄이 끝났는가”가 아니라 “언어별로 빠진 칸이 있는가”다.

어떤 문자열이 영어와 일본어만 채워지고 중국어 번체가 비어 있다면, 다음 번역 작업에서 빠진 언어만 다시 번역해야 한다.

sequenceDiagram
  autonumber
  participant Collector as 빠진 번역 수집기
  participant Store as 번역 캐시
  participant Provider as 번역 제공자

  Collector->>Store: 빠진 언어가 있는 한국어 원문 찾기
  Store-->>Collector: 번역할 원문
  Collector->>Provider: 빠진 언어만 번역
  Provider-->>Collector: 언어별 번역값
  Collector->>Store: 기존 언어를 지우지 않고 저장

이 방식은 한 언어의 실패가 영원한 빈칸으로 굳는 문제를 막는다. 이미 검수된 값이나 다른 언어 값을 지우지 않고, 부족한 곳만 채운다.

위험한 번역은 저장하지 않는다

기계 번역은 그대로 저장하지 않는다. 최소한의 차단선이 필요하다.

  • 원문 언어가 그대로 남아 있으면 저장하지 않는다.
  • 너무 긴 원문은 번역 할당량을 쓰지 않고 원문으로 둔다.
  • 언어별로 없는 값은 기존 값을 지우지 않는다.
  • 검수/공식 파일에 있는 고유명사는 기계 번역보다 우선한다.

기계 번역이 항상 자연스럽다고 보장할 수는 없다. 대신 위험한 번역을 저장하지 않고, 검수 파일로 덮어쓸 수 있게 둔다.

특히 장소명, 시설명, 행사명 같은 고유명사는 한 번 이상하게 번역되면 사용자가 바로 어색함을 느낀다. 그래서 캐시보다 사람이 덮어쓰고 검수할 수 있는 방법을 남겨야 했다.

API가 표시 텍스트를 골라 내려준다

클라이언트는 번역 DB를 직접 읽지 않는다. 백엔드가 언어 힌트를 받아 LocalizedText 형태로 화면에 쓸 표시 텍스트를 내려준다.

flowchart TB
  KRText["한국어 원문"] --> Resolver["번역 고르기"]
  Assets["공식/검수 번역 파일"] --> Resolver
  Cache["기계 번역 캐시"] --> Resolver
  Resolver --> DTO["API 표시 필드"]
  DTO --> Client["iOS 앱과 위젯"]

이렇게 나누면 iOS는 번역 저장소를 몰라도 된다. 앱은 백엔드가 고른 표시값과 원문을 대신 보여줬는지만 읽는다.

한국어 원문은 끝까지 남긴다

원문은 번역이 없을 때의 대체 문구이면서 나중에 되짚어 볼 수 있는 근거다. 공공데이터 앱에서 원문을 잃으면 “이 텍스트가 어디서 왔는가”를 설명하기 어렵다.

그래서 공공데이터는 한국어 원문과 출처 정보를 보존하고, 번역은 화면에 보여주는 값으로 둔다. 번역 실패가 수집 실패가 되어서는 안 된다.

이 구분은 나중에 고칠 때도 도움이 됐다. 번역이 이상하면 표시 텍스트를 고치면 되고, 원천 자체가 바뀌었으면 수집/정규화 쪽을 보면 된다. 원문을 남겨두면 문제를 다시 나눠 볼 수 있다.

빈칸을 보여주지 않는 구조

UI 문구와 공공데이터 번역은 따로 다뤄야 했다. 공공데이터 원문은 가장 믿을 수 있는 원본으로 남기고, 검수/공식 번역 파일은 기계 번역보다 우선하게 했다.

번역 캐시는 문자열 단위가 아니라 언어별 빠진 칸까지 봐야 했다. 번역 실패 시 key나 빈칸 대신 원문을 의도적으로 보여주고, 클라이언트가 번역 저장소를 직접 읽지 않게 하니 API 응답도 단순해졌다.

다국어 처리에서 오래 남은 선택은 모든 텍스트를 빠짐없이 번역하는 일이 아니었다. 원문을 잃지 않고, 더 나은 번역으로 갈아 끼울 수 있는 길을 남기고, 사용자가 빈칸을 보지 않게 만드는 일이었다.

이미지 확대