배치 로케일 오류 개선 사례

배치 로케일 오류 개선 사례

1. 들어가며

프로젝트를 수행하다 보면 요구사항을 처리하다 다른 문제를 발견하는 경우가 종종 있습니다. 이번 글에서는 마이페이지에 부서별 역할 정보를 추가하는 고객 요구사항을 처리하던 중 발견한 부서명 다국어 데이터 저장 오류와 이를 개선한 경험을 정리하고자 합니다.

2. 문제 발견 배경

문제는 마이페이지에 부서별 역할 정보를 표시하는 기능을 추가하던 중 발견했습니다. 고객 요구사항은 사용자가 속한 부서와 해당 부서에서 가진 역할을 마이페이지에서 확인할 수 있도록 하는 것이었습니다.

프론트엔드에서는 백엔드에서 내려주는 부서명과 역할 정보를 화면에 표시하면 되는 작업이었습니다. 그런데 개발 중 일부 부서명이 빈 값으로 넘어오는 현상을 확인했습니다. 처음에는 프론트엔드 매핑 문제나 응답 데이터 처리 문제를 의심했습니다. 하지만 API 응답을 확인해보니 프론트엔드에서 값을 누락한 것이 아니라, 백엔드에서 내려오는 부서명 자체가 비어 있었습니다.

이후 백엔드 조회 로직을 확인했습니다. 조회 로직은 현재 화면 언어에 맞춰 한국어인 ko 기준으로 부서명을 가져오고 있었습니다. 이 동작 자체는 정상적이었습니다. 문제는 DB에 저장된 다국어 데이터였습니다. DB를 직접 확인해보니 한국어 부서명이 ko가 아니라 en 언어 코드에 저장되어 있었습니다.

즉 화면은 한국어 부서명을 ko에서 찾고 있었지만, 실제 한국어 값은 en에 들어가 있었기 때문에 빈 값으로 보였던 것입니다.

3. 원인 분석

문제를 확인하면서 먼저 백엔드가 어떤 언어 코드를 기준으로 부서명을 조회하는지 확인했습니다. 조회 쪽은 한국어인 ko 기준으로 값을 가져오고 있었기 때문에, API 응답이 빈 값으로 내려오는 원인은 조회 조건보다는 저장된 데이터 쪽에 있다고 판단했습니다.

이후 부서 생성 경로를 따라가 보니, 병원 연동 배치에서 신규 부서를 Stage로 등록할 때 공통 등록 메서드를 사용하고 있었습니다. 이 메서드는 기본적으로 Spring의 LocaleContextHolder에서 현재 로케일을 가져와 언어 코드로 사용합니다.

public Tenant registerTenant(TenantCdo tenantCdo) { 
    return registerTenant( 
        tenantCdo, 
        LocaleContextHolder.getLocale().getLanguage() 
   ); 
} 

웹 요청에서는 요청 컨텍스트를 통해 로케일이 결정될 수 있지만, 배치는 사용자가 브라우저에서 직접 호출하는 흐름이 아닙니다. 따라서 배치 실행 시점에는 명확한 사용자 요청 로케일이 없었고, LocaleContextHolder가 JVM 기본 로케일을 사용하게 되었습니다. 해당 환경에서는 기본 로케일이 en으로 잡혀 있었고, 그 결과 병원 어댑터에서 내려준 한국어 부서명이 en 키에 저장되었습니다.

4. 신규 부서 등록 경로 개선

코드 수정은 병원 연동 배치에서 신규 부서를 Stage로 등록하는 경로에 초점을 맞췄습니다. 문제가 되었던 데이터는 배치가 새 부서를 만들 때 생성된 번역 데이터였기 때문에, 전체 부서 동기화 로직을 크게 바꾸기보다 신규 Stage 등록 시 언어 코드를 명확히 전달하는 방향으로 정리했습니다.

기존 registerTenant(TenantCdo) 메서드는 웹 요청에서 계속 사용할 수 있도록 유지했습니다. 대신 요청 로케일이 없거나 신뢰하기 어려운 배치, 동기화 경로에서는 언어 코드를 직접 받을 수 있도록 오버로드를 추가했습니다.

public Tenant registerTenant(TenantCdo tenantCdo) { 
    return registerTenant( 
        tenantCdo, 
        LocaleContextHolder.getLocale().getLanguage() 
   ); 
} 
 
public Tenant registerTenant( 
    TenantCdo tenantCdo, 
    String languageCode 
) { 
    Tenant tenant = Tenant.fromCdo( 
        tenantCdo, 
        languageCode 
   ); 
 
    return tenant; 
} 

실제 신규 Stage 생성 경로에서는 병원 어댑터가 내려주는 부서명이 한국어라는 점을 기준으로 ko를 직접 전달했습니다.

return (Stage) tenantLogic.registerTenant(stageCdo, "ko"); 

이렇게 하면 기존 웹 요청 경로에는 영향을 주지 않으면서, 배치에서 새로 생성하는 부서 데이터는 의도한 언어 코드로 저장할 수 있습니다. 기존 Stage 이름 변경이나 다른 수정 흐름까지 모두 바꾼 것이 아니라, 문제가 발생한 신규 등록 지점에 필요한 처리를 추가한 것입니다.

5. 데이터 보정 SQL

코드 수정은 이후 새로 생성되는 부서 데이터에 대한 조치이기 때문에, 이미 잘못 저장된 기존 데이터는 별도로 보정해야 했습니다.

DB를 확인한 결과, 해당 문제는 개발 환경에만 한정된 문제가 아니었습니다. 개발, 스테이징, 운영 환경 모두에서 동일하게 확인되었고, 원본 테이블뿐 아니라 조회 성능을 위해 별도로 구성된 조회용 데이터에도 같은 값이 반영되어 있었습니다.

데이터 보정 SQL은 운영 데이터에 영향을 최소화하는 방향으로 작성했습니다. 단순히 language_code = 'en'인 데이터를 모두 ko로 바꾸는 방식은 위험하다고 판단하여 다음 조건을 함께 적용했습니다.

  • 부서 데이터에 해당하는 항목만 수정합니다.
  • 유효한 데이터만 수정합니다.
  • 언어 코드가 en으로 저장된 데이터만 수정합니다.
  • 등록자가 시스템 계정인 데이터만 수정합니다.
  • 이미 같은 부서에 ko 번역이 존재하는 경우는 제외합니다.

실제 보정 쿼리는 다음과 같은 형태로 작성했습니다.

update cm_tenant_translation tt 
set language_code = 'ko', 
    modified_by = 'system-fix', 
    modified_on = now() 
from cm_tenant t 
where t.id = tt.tenant_id 
  and t.tenant_type = 'STAGE' 
  and tt.valid_yn = true 
  and tt.language_code = 'en' 
  and tt.registered_by = 'system' 
  and not exists ( 
    select 1 
    from cm_tenant_translation ko 
    where ko.tenant_id = tt.tenant_id 
      and ko.language_code = 'ko' 
      and ko.valid_yn = true 
  ); 

이 조건을 통해 실제 영어 데이터나 사용자가 직접 등록한 데이터가 잘못 변경되는 위험을 줄였습니다. 실제 Flyway 스크립트에서는 원본 테이블인 cm_tenant_translation뿐 아니라 조회용으로 사용되는 qm_tenant_view의 namei18n, qm_membership_view의 tenant_namei18n도 함께 보정했습니다.

update qm_tenant_view v 
set namei18n = (v.namei18n - 'en') 
    || jsonb_build_object('ko', v.namei18n ->> 'en'), 
    modified_on = now() 
where v.tenant_type = 'STAGE' 
  and v.namei18n ? 'en' 
  and not (v.namei18n ? 'ko'); 
 
update qm_membership_view m 
set tenant_namei18n = (m.tenant_namei18n - 'en') 
    || jsonb_build_object('ko', m.tenant_namei18n ->> 'en'), 
    modified_on = now() 
where m.tenant_type = 'STAGE' 
  and m.tenant_namei18n ? 'en' 
  and not (m.tenant_namei18n ? 'ko'); 

6. 적용 및 검증

개발계에서는 먼저 쿼리로 데이터를 수정한 뒤, 마이페이지 화면에서 부서별 역할 정보가 정상적으로 표시되는지 확인했습니다. 이때 단순히 DB 값만 확인하지 않고 실제 화면에서 부서명이 잘 나오는지 확인했습니다. 사용자가 경험하는 문제는 화면에서 발생했기 때문에, 최종 검증도 화면 기준으로 진행하는 것이 맞다고 판단했습니다.

스테이징과 운영 환경은 직접 쿼리를 수동으로 실행하기보다 Flyway 마이그레이션으로 반영했습니다. 운영 데이터에 영향을 주는 작업이기 때문에, 동일한 스크립트를 버전 관리하고 배포 이력에 남기는 방식이 더 안전하다고 보았습니다.

스테이징에서 먼저 Flyway 적용 결과를 확인한 뒤 운영에 반영했습니다. 적용 후에는 쿼리로 대상 데이터가 정상적으로 ko로 변경되었는지 확인하고, 화면에서도 부서명이 정상적으로 표시되는지 확인했습니다.

7. 배운 점 및 마무리

이번 작업을 통해 배치 처리는 일반 사용자 요청과 다른 기준으로 설계해야 한다는 점을 다시 확인했습니다. 웹 요청에서는 자연스럽게 존재하는 로케일, 사용자, 헤더 정보가 배치에서는 없거나 환경 기본값으로 대체될 수 있습니다. 배치에서 생성하는 데이터의 언어가 명확하다면 요청 컨텍스트에 기대기보다 코드에서 명시하는 편이 더 안전합니다.

또한 데이터 보정 작업에서는 수정 범위를 좁히는 조건이 중요했습니다. registered_by, tenant_type, valid_yn, 기존 ko 데이터 존재 여부를 함께 확인했기 때문에 시스템 배치가 만든 대상 데이터만 보정할 수 있었습니다.

이번 사례는 마이페이지 화면에서 일부 부서명이 빈 값으로 내려오는 작은 현상에서 시작했습니다. 하지만 화면, API 응답, DB 저장 구조, 배치 실행 환경을 함께 확인하면서 실제 원인을 찾고, 신규 데이터 생성 경로와 기존 데이터까지 함께 정리할 수 있었습니다.

Lynn

Site footer