업무 앱의 일본 연호・공휴일・마감일 처리 ── 연호 개원에 강한 설계와 JapaneseCalendar・영업일 계산의 실무

· · C#, .NET, 일본 연호(와레키), 공휴일, 마감일, 날짜/시간 처리, 업무 앱, Windows 개발, 기술 상담

「장표의 날짜는 연호로」「지급 기일은 20일 마감 익월 말일 지급, 말일이 휴일이면 전 영업일로」「집계는 회계연도 단위로」── 일본의 업무 앱에는 이 나라에서만 통용되는 날짜 요건이 표준으로 탑재되어 있습니다. 그리고 이 영역에는 일반적인 날짜/시간 처리와는 다른 종류의 함정이 숨어 있습니다. 연호는 개원 때마다 늘어나고, 공휴일은 법 개정으로 움직이며, 「월말」은 달마다 길이가 다릅니다. 즉 모두 「나중에 바뀌는 사양」인 것입니다.

지난번 「업무 앱의 날짜/시간과 시간대」에서는 UTC 저장의 원칙과 시간대 처리를 정리했습니다. 이번에는 그 후속편으로 일본 로컬의 3대 요건 ── 연호・공휴일・마감일 ── 을 다룹니다. 개원 대응 설계, 공휴일을 하드코딩해서는 안 되는 이유와 데이터로 관리하는 방법, AddMonths의 반올림 사양을 고려한 마감일 계산까지 구현 예시와 판단표로 살펴보겠습니다.

1. 먼저 결론

  • 내부 처리와 저장은 서기(그레고리력), 연호는 표시하는 순간에만. 연호 문자열로 저장하면 정렬 불가・개원 시 표기 혼재・잘못된 날짜 유입이라는 삼중고에 시달리게 됩니다.
  • 연호 변환은 자체 대응표가 아니라 JapaneseCalendar에 맡깁니다. 연호의 정의는 OS 쪽에서 공급되므로, 개원 시에는 플랫폼 업데이트로 따라올 수 있습니다.1 앱에 남은 「헤이세이」「레이와」 하드코딩만 뒤처지게 됩니다.
  • 공휴일은 계산식으로 판정할 수 없습니다. 춘분・추분은 전년도에 확정되고, 법 개정이나 특별조치법으로 공휴일 자체가 움직이기 때문입니다.2 공휴일 마스터+갱신 운영으로 설계하고, 데이터 소스로는 내각부가 공표하는 공휴일 CSV를 사용할 수 있습니다.
  • 「월말」「영업일」「회계연도」는 코드보다 먼저 사양으로 정의해야 하는 것입니다. 특히 AddMonths는 월말을 자동으로 반올림하므로, 마감일 계산은 기준일로부터 매번 계산하는 것이 원칙입니다.3

먼저 데이터 종류별 관리 방식을 정리합니다.

데이터 관리 방식 변환・판정 시점
날짜 그 자체(수주일・지급 기일) 서기의 DateOnly / DateTime(DB는 date형)
연호 표기(장표・화면) 저장하지 않음 표시・인쇄 순간에 JapaneseCalendar로 변환
공휴일 공휴일 마스터(테이블 또는 설정 파일) 영업일 계산 시 마스터 참조
회사 휴무일(여름휴가・창립기념일) 공휴일과는 별도의 휴무일 마스터 위와 동일(공휴일과 섞지 않음)
마감일・지급 조건 거래처 마스터의 속성(「20일 마감 익월 말일 지급」) 청구 처리 시 기준일로부터 계산
회계연도 저장하지 않음(도출값) 표시・집계 시 날짜로부터 계산

2. 연호 ── 표시 전용으로 만들기

2.1 왜 내부를 서기로 통일하는가

연호는 사람을 위한 표기이지, 계산에 적합한 표현이 아닙니다. 「헤이세이 9년」과 「레이와 9년」은 문자열 비교로는 순서를 정할 수 없고, 연호를 넘나드는 기간 계산에는 결국 서기로의 변환이 필요합니다. 게다가 연호는 개원할 때마다 체계가 늘어나므로, 연호 그대로 저장된 데이터는 개원 때마다 신구 표기가 뒤섞이게 됩니다. 「쇼와 64년 1월 10일」(실재하지 않음. 1월 8일부터 헤이세이)과 같은 잘못된 데이터의 유입도, 연호 입력을 그대로 저장하는 시스템에서 흔히 발생하는 문제입니다.

또 하나 간과하기 쉬운 제약으로, .NET의 JapaneseCalendar가 지원하는 범위는 메이지 원년(1868년) 9월 8일 이후입니다.4 그 이전 날짜 ── 예를 들어 호적에서 유래한 오래된 생년월일 ── 은 연호 그대로는 올바르게 처리할 수 없습니다. 내부를 서기 그레고리력으로 통일해 두면, 이러한 문제는 모두 「표시 계층만의 문제」로 국한됩니다.

2.2 JapaneseCalendar를 이용한 표시와 입력

.NET에서 여러 연호(era)를 다룰 수 있는 캘린더는 JapaneseCalendar(와 JapaneseLunisolarCalendar)뿐이며, 서식 지정자 g가 연호명에 대응합니다.1 연호로 표시하는 최소 코드는 다음과 같습니다.

using System.Globalization;

var jaJpWareki = new CultureInfo("ja-JP");
jaJpWareki.DateTimeFormat.Calendar = new JapaneseCalendar();

var date = new DateOnly(2026, 7, 11);
Console.WriteLine(date.ToString("ggy年M月d日", jaJpWareki));  // 令和8年7月11日
Console.WriteLine(date.ToString("ggyy/MM/dd", jaJpWareki));   // 令和08/07/11

연호 입력을 받을 경우에는 자유 입력 문자열 파싱이 아니라 「연호는 콤보박스로 선택하게 하고, 연월일은 숫자로 받는」 UI로 만드는 것이 안전합니다. 부득이하게 문자열로 받아야 한다면 ParseExact로 서식을 고정합니다.

var input = "令和8年7月11日";
var parsed = DateTime.ParseExact(input, "ggy年M月d日", jaJpWareki);
Console.WriteLine(parsed.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));  // 2026-07-11

원년 표기에는 주의가 필요합니다. 개원 대응 업데이트가 적용된 환경에서는 각 연호의 1년차가 기본적으로 「레이와 원년」처럼 원년으로 정형화되며, 숫자 표기인 「레이와 1년」으로 되돌리기 위한 호환 스위치(Switch.System.Globalization.FormatJapaneseFirstYearAsANumber)가 마련되어 있습니다.5 장표 양식이 「레이와 1년」과 「레이와 원년」 중 어느 쪽을 요구하는지는 구현 전에 사양으로 확인해야 합니다. 또 하나, 기본적으로 연호의 연도 범위 체크는 완화(relaxed)되어 있어, 「헤이세이 33년」과 같이 실재하지 않는 연도 지정도 받아들여져 실제 날짜(레이와 3년)로 환산됩니다. 입력 검증으로 엄격하게 걸러내고 싶다면 Switch.System.Globalization.EnforceJapaneseEraYearRanges로 강제할 수 있습니다.5 완화 상태로 둘지 엄격하게 할지는 기존 데이터의 오염 정도(개원 전에 인쇄・입력된 「헤이세이 32년」을 어떻게 다룰지)로 결정하게 됩니다.

2.3 개원에 강한 코드와, 뒤처지는 것들

레이와 개원(2019년)의 경험에서 말할 수 있는 것은, 플랫폼은 따라오지만 앱의 자체 구현은 따라오지 않는다는 점입니다. 연호의 정의는 .NET에 하드코딩되어 있지 않고 OS 쪽이 가진 연호 정보를 참조하는 구조이므로, OS와 .NET의 업데이트를 적용하고 있다면 JapaneseCalendar의 변환・정형화는 새 연호를 따라옵니다.1 뒤처지는 것은 다음과 같은 앱 쪽의 자산입니다.

  • if (year >= 2019) era = "令和";와 같은 자체 연호 변환 테이블・분기
  • 장표 템플릿・Excel 셀 서식・인쇄된 용지에 직접 쓰인 연호
  • 「R」「H」 등 연호 알파벳 한 글자를 독자 규칙으로 다루는 코드(기간계 시스템과의 연계 포맷에 많음)
  • 연호 그대로 저장된 데이터 자체

개원 대응 개수 견적을 낼 때는 먼저 소스와 템플릿 전체를 「쇼와・헤이세이・레이와・원년」으로 기계적으로 grep하여 조사합니다. VB6이나 Access 시절부터 동작하는 업무 앱에는 자체 변환 테이블이 거의 확실히 묻혀 있으므로, 「VB6 / Access 업무 앱의 수명 연장과 마이그레이션」에서 다룬 존속 판단과 함께 다루는 경우가 많은 주제입니다.

2.4 Windows 지역 설정이라는 복병

간과하기 쉬운 함정이 하나 더 있습니다. Windows의 지역 설정에서는 사용자가 OS 캘린더의 종류를 「연호」로 변경할 수 있습니다. CultureInfo.CurrentCulture는 기본적으로 사용자의 설정 변경(사용자 오버라이드)을 반영하므로,6 이 설정이 된 단말기에서는 컬처 기본값의 DateTime.ToString()이 갑자기 「令和08/07/11」과 같은 연호 문자열을 반환하게 됩니다. 컬처 기본값의 ToString()으로 로그나 CSV, DB에 날짜를 출력하는 코드가 있으면, 그 단말기에서만 저장 데이터에 연호가 섞여 이후 파싱이 깨집니다.

대책은 지난 글과 같은 결론에 이릅니다. 경계(저장・통신・로그)를 넘는 날짜 문자열은 반드시 CultureInfo.InvariantCulture와 명시적 서식으로 출력해야 합니다. 컬처 기본 서식은 화면 표시 전용입니다. 컬처가 표시에 미치는 영향 전반은 「WinForms/WPF 앱의 다국어화」도 참고할 수 있습니다.

3. 공휴일 ── 하드코딩해서는 안 되는 이유와 마스터 운영

3.1 공휴일은 「바뀐다」

공휴일 판정을 switch (월, 일)로 작성해서는 안 됩니다. 공휴일은 국민의 축일에 관한 법률로 정해져 있으며, 법 개정과 운영에 따라 여러 차례 바뀌어 왔기 때문입니다.2 최근 사례만 보아도 다음과 같습니다.

연도 변경 내용
2016 산의 날(8월 11일) 신설
2019 개원에 따라 천황탄생일이 12월 23일 → 2월 23일로. 2019년에는 천황탄생일이 존재하지 않음
2019 황위 계승에 따른 특례로 5월 1일(즉위일) 등이 임시 공휴일・휴일로
2020 체육의 날이 스포츠의 날로 개칭
2020・2021 올림픽 특별조치법으로 바다의 날・스포츠의 날・산의 날이 해당 연도에만 이동

또한 춘분의 날과 추분의 날은 애초에 법률에 날짜가 쓰여 있지 않습니다. 천문 계산에 기반하여 전년도에 확정・공표되는 구조이므로, 미래의 춘분・추분을 확실히 알 방법은 공표를 기다리는 것뿐입니다.2 「공휴일은 매년 같다」는 전제의 계산식은 원리적으로 성립하지 않습니다.

3.2 데이터 소스와 관리 방식 판단표

공휴일 판정은 「데이터+갱신 운영」으로 설계합니다. 주요 선택지를 비교합니다.

방식 내용 적합한 경우 주의점
공휴일 마스터(자체 관리) 테이블에 연월일과 명칭을 두고 관리 화면에서 갱신 회사 휴무일과 함께 운영하고 싶은 업무 시스템 전반 갱신을 업무 운영에 편입(연 1회 확인을 캘린더화)
내각부 CSV 반영 내각부 공표 공휴일 CSV를 반영하여 마스터 갱신 마스터 갱신의 1차 소스가 필요한 경우(권장) 공표된 기간만 포함됨. 문자 인코딩에 주의
공휴일 라이브러리(NuGet 등) 계산 로직+수록 데이터로 판정 사내 도구 등 갱신 운영을 유지할 수 없는 소규모 용도 향후 법 개정은 라이브러리 업데이트에 의존. 수록 규칙의 검증 필요
하드코딩 소스에 날짜를 직접 기재 없음 법 개정 때마다 전체 클라이언트 재배포 필요

1차 소스로는 내각부가 「국민의 축일」 페이지에서 공표하는 쇼와 30년(1955년)부터 다음 해분까지의 공휴일 월일・명칭 CSV를 사용할 수 있습니다.2 반영 시 실무적으로 주의할 점은 두 가지로, (1) 공표되는 것은 확정분뿐이므로 2년 후 이후의 공휴일을 조회할 때의 동작(오류로 처리할지, 법률 기반 잠정값으로 채우고 「잠정」 플래그를 붙일지)을 사양으로 정해둘 것, (2) 이 CSV의 문자 인코딩은 Shift_JIS 계열이므로 읽어들일 때 「CSV는 「그냥 텍스트」가 아니다」에서 다룬 것처럼 인코딩을 명시할 것입니다.

3.3 대체휴일・국민의 휴일과 영업일 계산

공휴일법에는 날짜 목록 외에 두 가지 파생 규칙이 있습니다. 대체휴일(공휴일이 일요일에 해당할 때 그 다음의 가장 가까운 「공휴일이 아닌 날」을 휴일로 하는 것)과 국민의 휴일(전날과 다음 날이 공휴일로 끼인 평일을 휴일로 하는 것)입니다.2 내각부 CSV에는 이것들도 행으로 포함되어 있으므로, CSV 반영 방식이라면 마스터 참조만으로 충분합니다. 자체적으로 계산을 보완하는 경우에도 이 로직은 법률에서 유래하므로 비교적 안정적입니다.

영업일 계산은 「주말이 아니다・공휴일 마스터에 없다・회사 휴무일 마스터에 없다」의 세 가지 조건으로 작성합니다.

public sealed class BusinessDayCalendar
{
    private readonly HashSet<DateOnly> _publicHolidays;   // 공휴일 마스터(대체 공휴일 포함)
    private readonly HashSet<DateOnly> _companyHolidays;  // 회사 휴무일 마스터

    public BusinessDayCalendar(IEnumerable<DateOnly> publicHolidays,
                               IEnumerable<DateOnly> companyHolidays)
    {
        _publicHolidays = [.. publicHolidays];
        _companyHolidays = [.. companyHolidays];
    }

    public bool IsBusinessDay(DateOnly d) =>
        d.DayOfWeek is not (DayOfWeek.Saturday or DayOfWeek.Sunday)
        && !_publicHolidays.Contains(d)
        && !_companyHolidays.Contains(d);

    // 기한일이 휴일이면 '이전 영업일로 앞당긴다'. 뒤로 미룰지는 계약 사양으로 결정
    public DateOnly ToPreviousBusinessDay(DateOnly d)
    {
        while (!IsBusinessDay(d)) d = d.AddDays(-1);
        return d;
    }
}

공휴일과 회사 휴무일을 별도의 마스터로 나누는 것은 의도적입니다. 「은행 송금 기일 판정은 공휴일만 본다(자사의 여름휴가는 관계없다)」「출하일 판정은 둘 다 본다」처럼, 용도에 따라 참조하는 휴일의 집합이 다르기 때문입니다. 하나의 마스터에 섞어버리면 나중에 분리하는 개수는 이미 저장된 데이터의 고고학이 되어버립니다.

4. 마감일・지급 조건・회계연도 ── 「월말」을 계산으로 올바르게 다루기

4.1 AddMonths의 반올림 사양

마감일 계산의 첫 번째 함정은 AddMonths입니다. DateTime.AddMonths / DateOnly.AddMonths는 결과 월에 존재하지 않는 날이 될 경우 그 달의 마지막 날로 반올림합니다.3

var jan31 = new DateOnly(2026, 1, 31);
var feb = jan31.AddMonths(1);   // 2026-02-28 ── 월말로 반올림됨(올바른 동작)
var mar = feb.AddMonths(1);     // 2026-03-28 ── '월말'을 의도했지만 28일로 바뀌어버림

반올림 자체는 올바른 사양이지만, 반올림된 결과에 다시 AddMonths를 거듭하면 「월말」이라는 의도가 사라집니다. 매월 청구 스케줄을 「직전 실행일+1개월」로 짜면, 2월을 통과하는 순간부터 영구히 28일 실행으로 변해버리는 형태로 드러납니다. 대책은 두 가지입니다.

  • 반복되는 날짜는 직전 결과가 아니라 기준의 정의(「매월 말」「매월 20일」)로부터 매번 계산한다
  • 「월말」은 날짜 리터럴이 아니라 DateTime.DaysInMonth(y, m)로부터 도출한다

4.2 「20일 마감 익월 말일 지급」의 구현

마감일과 지급 조건은 거래처별 속성입니다. 「20일 마감 익월 말일 지급」을 코드로 만들면 다음과 같습니다.

public static class PaymentTerms
{
    // 거래일 → 마감일(매월 20일 마감 예시)
    public static DateOnly GetClosingDate(DateOnly tradeDate, int closingDay)
    {
        var dayInMonth = Math.Min(closingDay, DateTime.DaysInMonth(tradeDate.Year, tradeDate.Month));
        var closing = new DateOnly(tradeDate.Year, tradeDate.Month, dayInMonth);
        return tradeDate <= closing ? closing : closing.AddMonths(1);
        // 참고: closingDay=31(월말 마감)은 AddMonths의 반올림으로 인해 다음 달 말일이 정상적으로 반영됨
    }

    // 마감일 → 지급 기일(익월 말일 지급 예시). 휴일 조정은 사양(앞당김/뒤로 미룸)을 확인 후 적용
    public static DateOnly GetPaymentDueDate(DateOnly closingDate, BusinessDayCalendar calendar)
    {
        var nextMonth = closingDate.AddMonths(1);
        var endOfMonth = new DateOnly(nextMonth.Year, nextMonth.Month,
                                      DateTime.DaysInMonth(nextMonth.Year, nextMonth.Month));
        return calendar.ToPreviousBusinessDay(endOfMonth);  // '휴일이면 이전 영업일'이라는 계약 조건의 경우
    }
}

코드보다 중요한 것은 그 이전 단계인 사양의 확정입니다. 「월말 마감」의 월말은 역일 기준인지 영업일 기준인지. 기일이 휴일일 때 앞당길지 뒤로 미룰지(지급하는 쪽은 앞당기고, 받는 쪽의 입금 예정은 뒤로 미루는 등 입장에 따라 관행이 다릅니다). 29~31일 마감 계약은 2월에 어떻게 해석할지. ── 이런 부분이 문서화되어 있지 않으면 구현자의 코드가 그대로 「아무도 합의하지 않은 사양」이 되어버립니다. 마감일 관련 버그 조사의 상당수는 코드의 오류가 아니라 사양의 부재로 귀결됩니다.

4.3 회계연도와 분기

일본 업무 앱의 「회계연도」는 거의 4월 시작입니다. 회계연도는 저장하지 않고 날짜로부터의 도출값으로 다룹니다.

public static int GetFiscalYear(DateOnly d) => d.Month >= 4 ? d.Year : d.Year - 1;
// 2026-03-31 → 2025년도, 2026-04-01 → 2026년도

public static int GetFiscalQuarter(DateOnly d) => ((d.Month + 8) % 12) / 3 + 1;
// 4-6월 → Q1, 7-9월 → Q2, 10-12월 → Q3, 1-3월 → Q4

주의할 점은 「회계연도」를 안이하게 DB에 중복 저장하지 않는 것입니다. 날짜만 있으면 항상 도출할 수 있는 값을 별도 컬럼에 두면, 날짜 수정 시 갱신 누락으로 날짜와 회계연도가 어긋나는 데이터가 생겨납니다. 집계 성능을 위해 부득이하게 가져야 한다면 계산 열(생성 열)로 만들어 앱에서의 쓰기를 금지합니다.

4.4 테스트 ── 「그 날에만」 발생하는 버그를 먼저 밟아보기

연호・공휴일・마감일 버그는 특정 날짜에서만 발생합니다. 지난 글에서 소개한 TimeProvider를 이용한 현재 시각 주입(「업무 앱의 날짜/시간과 시간대」 7장)을 전제로, 이 영역에서 최소한 테스트해야 할 날짜는 다음 다섯 가지입니다.

  • 개원 경계: 1989-01-07/08(쇼와→헤이세이), 2019-04-30/05-01(헤이세이→레이와)의 표시와, 연호 입력의 역변환
  • 원년: 「레이와 원년」 표기의 장표 확인(2.2절)
  • 윤년: 2월 29일을 포함하는 마감・지급 조건 계산(2024년, 2028년)
  • 월말의 연쇄: 1월 31일을 기점으로 12개월분의 마감일을 생성하여 월말이 유지되는지(4.1절)
  • 회계연도 경계: 3월 31일과 4월 1일의 회계연도・분기 판정

또한 리뷰 시에는 다음 패턴을 grep으로 기계적으로 찾아낼 수 있습니다.

발견하면 의심해야 할 코드 무슨 일이 일어날 수 있는가 고치는 방법
"平成" "令和" 등 연호 리터럴 다음 개원 때 뒤처짐 JapaneseCalendar + g 서식으로(2.3절)
공휴일의 날짜 리터럴(5, 3 등) 법 개정・특별조치법으로 오판정 공휴일 마스터 참조로(3.2절)
인수 없는 ToString()의 저장・출력 경로 연호 설정 단말기에서 저장 데이터에 연호 혼입 InvariantCulture + 명시적 서식(2.4절)
직전날짜.AddMonths(1)의 반복 2월 통과 후 월말이 계속 어긋남 기준일로부터 매번 계산(4.1절)
new DateTime(y, m, 31) 30일 이하인 달에서 예외 발생 DateTime.DaysInMonth로부터 도출

5. 정리

연호・공휴일・마감일은 모두 「나중에 바뀌는 사양」을 상대하는 처리입니다. 그래서 설계 원칙도 공통됩니다. 바뀌는 것은 코드에 쓰지 않고 데이터와 변환 계층에 격리한다 ── 내부와 저장은 서기로 통일하여 연호는 표시 전용 변환으로 만들고, 공휴일은 마스터와 갱신 운영으로 관리하며, 「월말」「영업일」「기일의 휴일 조정」은 계약 사양으로 문서화한 뒤 기준일 기반으로 계산합니다. 여기까지 해두면 다음 개원도, 다음 공휴일 법 개정도, 앱 입장에서는 「마스터 갱신」과 「OS 업데이트 적용」만으로 끝나는 이벤트가 됩니다.

당사는 장표・청구 관련 날짜 처리의 설계와 구현, 기존 앱에 묻혀 있는 연호・공휴일 하드코딩 조사와 개수, 마감일 사양 정리를 포함한 설계 리뷰를 다루고 있습니다. 「개원 대응이 어디에 몇 건 있는지 모른다」는 단계에서부터도 도움을 드릴 수 있습니다.

관련 글

관련 상담 분야

합동회사 코무라소프트에서는 일본의 상관행에 맞춘 업무 앱의 날짜 처리(연호 장표・영업일 계산・마감일 처리)의 설계와 구현, 기존 앱의 개원 대응・공휴일 대응 조사와 개수를 다루고 있습니다.

참고 링크

  1. Microsoft Learn, Work with eras - .NET. JapaneseCalendar와 JapaneseLunisolarCalendar가 여러 연호(era)를 인식하는 유일한 캘린더 클래스라는 점, 서식 지정자 「g」에 의한 연호 정형화, 연호 정보가 플랫폼 쪽에서 공급되어 앱의 하드코딩에 의존하지 않는다는 점에 대해.  2 3

  2. 일본 내각부, 「국민의 축일」에 대하여. 국민의 축일에 관한 법률에 기반한 공휴일 목록, 춘분의 날・추분의 날이 전년도에 확정・공표된다는 점, 대체휴일・국민의 휴일 규정, 쇼와 30년부터 다음 해까지의 공휴일 월일・명칭 CSV 제공에 대해.  2 3 4 5

  3. Microsoft Learn, DateTime.AddMonths(Int32) Method. 더한 결과의 일자가 결과 월의 일수를 초과하는 경우, 결과 월의 마지막 날로 조정된다는 점(1월 31일+1개월이 2월 28일/29일이 되는 예)에 대해.  2

  4. Microsoft Learn, JapaneseCalendar Class. JapaneseCalendar가 지원하는 날짜 범위가 메이지 원년(서기 1868년) 9월 8일 이후라는 점, 연호(era) 처리에 대해. 

  5. Microsoft Learn, Work with calendars - .NET. 연호의 1년차를 「원년」으로 정형화하는 기본 동작과 숫자 표기로 되돌리는 Switch.System.Globalization.FormatJapaneseFirstYearAsANumber 스위치, 연호의 연도 범위 체크가 기본적으로 완화되어 있으며 Switch.System.Globalization.EnforceJapaneseEraYearRanges로 엄격화할 수 있다는 점에 대해.  2

  6. Microsoft Learn, CultureInfo.CurrentCulture Property. CurrentCulture가 기본적으로 Windows 사용자의 지역 설정 변경(사용자 오버라이드)을 반영한다는 점, 서식 정형화의 기본값이 이 컬처에 의존한다는 점에 대해. 

같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.

이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.

이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.

Windows 앱 개발

장표의 연호 표시, 영업일 계산, 마감일 처리와 같은 일본 특유의 업무 요건 구현은 Windows 업무 앱 개발 상담의 핵심 범위이기 때문입니다.

자주 묻는 질문

이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.

데이터베이스에 연호를 그대로 저장하면 안 되나요?
권장하지 않습니다. 이유는 네 가지입니다. 첫째, 「헤이세이 9년」과 「레이와 9년」처럼 연호가 바뀌는 지점을 넘어가면 문자열 비교로는 대소 관계가 깨져 정렬도 범위 검색도 불가능해집니다. 둘째, 다음 개원이 일어나는 순간 이미 저장된 데이터와 새 데이터의 표기가 뒤섞입니다. 셋째, 「쇼와 64년 1월 10일」(실재하지 않음. 1월 8일부터 헤이세이 원년)처럼 존재하지 않는 날짜가 섞인 오염된 데이터가 유입되기 쉬워, 파싱이 실패하는 시한폭탄이 됩니다. 넷째, .NET의 JapaneseCalendar가 다룰 수 있는 범위는 메이지 원년(1868년) 9월 8일 이후이므로, 그 이전의 생년월일 등은 연호 그대로는 올바르게 처리할 수 없습니다. 내부 처리와 저장은 서기(그레고리력)로 통일하고, 연호는 화면과 장표에 출력하는 순간에만 변환하는 것이 원칙입니다.
다음 연호 개원이 오면 앱은 어떻게 대비해야 하나요?
연호의 정의 자체는 .NET에 하드코딩되어 있지 않고 OS 쪽의 연호 정보를 참조하는 구조이므로, OS와 .NET의 업데이트를 적용하고 있다면 「새 연호로의 표시」 자체는 자동으로 따라옵니다. 레이와 개원 때도 대응의 핵심은 플랫폼 업데이트 적용이었습니다. 문제는 앱 쪽의 코드와 데이터입니다. 「헤이세이」「레이와」를 문자열로 하드코딩한 분기, 연호와 서기의 대응표를 직접 가진 변환 함수, 장표 템플릿에 직접 쓰인 연호, 인쇄된 서식의 「레이와 __년」란 등은 자동으로 따라오지 않습니다. 대비책으로는 (1) 내부・저장을 서기로 통일하여 연호를 표시 전용으로 만들기, (2) 자체 연호 변환을 그만두고 JapaneseCalendar에 맡기기, (3) 소스 전체를 「쇼와・헤이세이・레이와」로 grep하여 조사하기, (4) 가상의 연호 전환을 가정한 테스트를 준비하기, 이 네 가지가 현실적입니다.
공휴일은 계산식으로 판정할 수 없나요? 왜 데이터로 가지고 있어야 하나요?
완전한 계산식은 원리적으로 만들 수 없습니다. 이유는 세 가지입니다. 첫째, 춘분의 날과 추분의 날은 천문 계산에 기반하여 각의를 거쳐 전년도에 확정되는 구조로, 법률에 고정된 날짜로 쓰여 있지 않습니다. 둘째, 공휴일은 법 개정으로 추가・이동・개칭됩니다. 실제로 2016년 산의 날 신설, 2019년 천황탄생일이 12월 23일에서 2월 23일로 이동(**2019년에는 천황탄생일이 없었음**), 체육의 날에서 스포츠의 날로의 개칭이 있었습니다. 셋째, 2020년・2021년에는 특별조치법으로 바다의 날・스포츠의 날・산의 날이 올림픽에 맞춰 이동하여, 「매년 셋째 월요일」과 같은 규칙 자체가 임시로 덮어써지는 경우도 있습니다. 따라서 공휴일 판정은 「공휴일 마스터(데이터)+갱신 운영」으로 설계하고, 계산 로직은 대체휴일 등의 보조에 그치는 것이 정답입니다.
AddMonths(1)을 사용했더니 지급 기일이 월말에서 어긋났습니다. 왜 그런가요?
DateTime.AddMonths는 계산 결과가 해당 월에 존재하지 않는 날이 될 경우 그 달의 마지막 날로 반올림하는 사양이기 때문입니다. 1월 31일에 AddMonths(1)을 적용하면 2월 28일(윤년은 29일)이 됩니다. 이것 자체는 올바른 동작이지만, 반올림된 결과를 다시 다음 달로 넘겨 AddMonths를 적용하면 2월 28일에 AddMonths(1)을 적용한 결과는 3월 28일이 되어, 「월말이려던 것이 28일로 변해버린」 채로 이후 계속 어긋납니다. 대책은 반복 계산에서 이전 결과에 더하지 말고 반드시 기준일(계약일이나 「매월 말」이라는 정의)로부터 매번 계산하는 것, 그리고 「월말」을 날짜 리터럴이 아니라 DateTime.DaysInMonth로 도출하는 것, 이 두 가지입니다.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기