LabHub
배우기 러닝패스 코스

은행 현장의 언어 · 타임존·영업일·마감시각 · 이론

어제 거래가 시스템마다 다르게 세어졌다

LabHub 에서 이어서 보기

한 줄 요약

순간(instant)과 달력 날짜는 다른 것이다. 은행의 결제일은 순간을 어느 표준시의 달력에 올려놓고, 마감시각영업일 달력을 적용해 계산해 내는 값이다.

왜 이게 필요했나

"어제 거래 몇 건이었나" 라는 질문에 시스템 세 곳이 서로 다른 숫자를 냈다고 해 보자. 하나는 UTC 자정으로 잘랐고, 하나는 KST 자정으로 잘랐고, 하나는 전날 마감시각부터 오늘 마감시각까지로 세었다. 셋 다 자기 기준으로는 옳다. 그래서 이 어긋남은 장애로 보이지 않고 "집계가 좀 다르네" 로 묻힌다. 묻혀 있다가 정산 대사에서 터진다.

원인은 거의 언제나 같다. 데이터에는 접수 순간만 있는데, 사람과 규정은 날짜로 말한다. 순간은 지구 어디서 재도 하나지만, 그 순간이 며칠인지는 표준시를 정해야 비로소 답이 나온다. 이 변환을 코드 여기저기에서 즉석으로 하면, 변환 규칙이 코드 개수만큼 생긴다.

여기에 은행의 사정이 하나 더 붙는다. 돈이 실제로 오가는 날, 곧 결제일(value date) 은 접수한 날과 다를 수 있다. 마감시각(cutoff)이 지난 뒤 들어온 요청은 다음 영업일로 넘어가고, 그 다음 날이 토요일이거나 휴장일이면 또 밀린다. 그래서 결제일은 저장해 두는 사실이 아니라 규칙의 산출물이다.

어떻게 동작하나

설계는 세 겹으로 나뉜다.

1. 저장은 순간으로. 접수 시각은 UTC 로, [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html) 의 2026-07-02T07:30:00Z 모양으로 남긴다. RFC 3339 는 오프셋이 없는 시각을 "지역시각" 으로 부르며 그것만으로는 순간을 특정할 수 없다고 못박는다. 오프셋 없는 문자열을 DB 에 넣는 순간 그 열은 더 이상 순간이 아니다.
2. 표시와 버킷팅은 지역시각으로. 파이썬 [zoneinfo](https://docs.python.org/3/library/zoneinfo.html) 는 운영체제의 IANA 시간대 데이터베이스를 읽어 ZoneInfo("Asia/Seoul") 을 만든다. astimezone() 으로 옮긴 뒤 .date() 를 취하면 그 순간이 서울 달력에서 며칠인지가 나온다. 고정으로 9시간을 더하는 코드와 결과가 갈리는 지점이 바로 여기다.
3. 결제일은 규칙으로. 지역시각의 시분이 마감시각 이상이면 하루를 더하고, 그 날짜부터 영업일을 찾아 앞으로 민다. 영업일 판정은 주말과 휴장일 달력에서 나온다.

고정 오프셋이 왜 위험한지는 한국 자료로 바로 보인다. Asia/Seoul 은 지금 UTC+9 고정이지만 언제나 그랬던 것은 아니다. 실습 이미지에서 zoneinfo 로 직접 재 보면 이렇다.

1987-05-15 12:00 Asia/Seoul  ->  UTC+10 (KDT)   # 실습 이미지에서 실측1987-12-15 12:00 Asia/Seoul  ->  UTC+9  (KST)   # 실습 이미지에서 실측2026-07-15 12:00 Asia/Seoul  ->  UTC+9  (KST)   # 실습 이미지에서 실측전환 순간: 1987-05-10 02:00 에 +10 으로, 1987-10-11 03:00 에 +9 로 (1988년도 같은 모양)

1987·1988년 여름에 한국에 서머타임이 있었다는 사실이 tzdb 에 그대로 들어 있고, 그래서 zoneinfo 는 그 구간에 UTC+10 을 돌려준다. 고정 9시간을 더하는 코드는 이 구간의 이관 데이터를 한 시간씩 틀리게 읽는다. 자정 근처의 거래라면 날짜가 통째로 하루 어긋난다. [tzdb 의 설계 문서](https://data.iana.org/time-zones/tzdb/theory.html)는 시간대 정의가 정치적 결정으로 계속 바뀌며 과거 기록도 새로 밝혀지면 고쳐진다고 적는다. 그래서 규칙은 코드가 아니라 데이터에 있어야 하고, 그 데이터는 [IANA 시간대 데이터베이스](https://www.iana.org/time-zones)가 갱신한다.

날짜 산술을 DB 에 맡길 때도 같은 함정이 있다. SQLite 의 [날짜·시각 함수](https://www.sqlite.org/lang_datefunc.html)는 localtime 수정자를 제공하지만 그것은 서버의 지역시각이지 업무가 정한 표준시가 아니다. 컨테이너의 TZ 설정 하나로 결제일이 바뀌는 시스템을 만들고 싶지 않다면, 표준시는 설정값으로 명시하고 변환은 애플리케이션이 한다.

현장에서 만나는 모습

가장 흔한 사고는 "UTC 날짜를 그대로 업무일자로 쓴" 경우다. 개발 환경에서는 하루 종일 UTC 와 KST 의 날짜가 같아 보이는 시간대에만 시험하다가, 운영에서 저녁 9시(= UTC 12시) 이후 거래가 쌓이면서 드러난다. KST 09:00 이전, 곧 UTC 자정 전후의 거래가 전날로 붙는 것이다.

두 번째는 마감시각을 UTC 로 비교하는 경우다. received_utc[11:16] >= "16:30" 같은 비교는 지금은 우연히 답이 맞을 수도 있지만, 표준시가 다른 상대 기관의 마감을 같은 코드로 다루는 순간 무너진다. 비교는 반드시 지역시각으로 옮긴 뒤 한다.

세 번째는 상대방의 날짜다. 서울에서 7월 3일 아침에 보낸 전문이 런던에서는 아직 7월 2일이다. 상대 기관 파일의 value_date 를 우리 달력으로 읽어 대사하면 하루치가 통째로 미결제로 남는다. [datetime 문서](https://docs.python.org/3/library/datetime.html)가 구분하는 aware 와 naive 의 차이가 실무에서 돈으로 바뀌는 자리다.

실무에서 진짜 중요한 것

다음 실습에서 할 것

열이틀치 이체 접수 원장을 직접 만들고, 지금 기간계가 쓰는 "UTC 날짜 = 결제일" 규칙이 몇 건을 틀리게 만드는지 셉니다. 휴장일 달력과 마감시각을 읽는 결제일 계산 엔진을 만들어 영업일 가감, 마감시각 판정, 상대방 표준시 변환을 차례로 붙이고, 1987·1988년 이관분에서 서머타임 구간을 집어냅니다. 마지막에는 원장을 덮어쓰지 않고 결제일을 전 건 재계산해 무엇이 왜 바뀌는지 보고서로 냅니다.