기술

공휴일마다 마감 문의가 반복됐습니다 — 달력일 -2가 범인이었습니다

2026.07.126분 읽기

공휴일이 낀 주만 되면 같은 문의가 반복됐습니다. "아직 마감 전인데 왜 예약이 안 되나요", "어제까지는 취소가 됐는데 오늘은 왜 막히나요." 운영 담당자는 담당자대로 마감이 어긋난 주문을 수동으로 정리하고 있었습니다. 임직원 식사 예약 서비스의 두 예약 서비스(테이크아웃·라운지)를 운영하며 겪은 일입니다.


달력일 -2는 공휴일을 모른다

원인은 마감 계산식 하나였습니다. 예약·취소 마감이 "제공일에서 달력일 -N일"로 계산되고 있었습니다. 목요일 제공분을 -2 달력일로 잡으면 화요일 마감입니다. 그런데 그 사이 수요일이 공휴일이라면 현장은 실제로 하루를 통째로 쉬게 됩니다. 준비 시간을 확보하려면 마감이 더 앞으로 당겨져야 하는데, 시스템은 그대로 화요일을 가리켰습니다.

사용자와 운영은 영업일 감각으로 움직입니다. 은행 이체나 택배 안내가 "영업일 기준 2일"이라고 말하는 이유와 같습니다. 그런데 시스템만 달력을 세고 있으니 공휴일이 낄 때마다 사람의 감각과 시스템의 계산이 어긋났습니다.

공휴일마다 마감 문의가 반복됐습니다 — 달력일 -2가 범인이었습니다 그림 1


영업일 역산으로 교체

마감 계산을 영업일 역산으로 바꿨습니다. 제공일에서 거꾸로 N영업일을 세되 토·일과 공휴일은 건너뜁니다. 목요일 제공분에서 2영업일을 역산하면, 수요일이 공휴일일 때 마감은 화요일이 아니라 월요일이 됩니다.

예약 오픈, 즉 먼 쪽 경계도 함께 영업일화했습니다. 공휴일·주말이 낀 만큼 오픈 시점이 자동으로 앞당겨집니다. 회원앱에는 예약 가능 범위를 별도 응답으로 내려 날짜 탭과 안내 문구에 그대로 반영했습니다. 계산은 복잡해졌지만 현실과 맞습니다.


주말은 코드로, 공휴일만 테이블로

영업일 역산에는 "쉬는 날" 데이터가 필요합니다. 처음엔 주말까지 전부 테이블에 넣을지 고민했지만, 기준을 하나 세웠습니다. 규칙으로 판정되는 것은 코드가 맡고, 불규칙한 것만 데이터가 맡는다는 분담입니다.

토·일요일: 매주 반복되는 규칙이므로 코드에서 요일로 판정

공휴일·대체공휴일: 해마다 달라지는 불규칙 값이므로 전역 공휴일 테이블에 등록

임시공휴일: 규칙화 자체가 불가능하므로 역시 데이터로 보정

이 분담 덕분에 관리할 데이터가 최소화됩니다. 주말까지 데이터로 넣었다면 연간 100건이 넘는 행을 관리했을 텐데, 공휴일만 넣으니 연 15건 안팎입니다. 대신 공휴일 테이블은 사람이 매년 갱신해야 합니다. 갱신을 놓치면 조용히 틀리기 때문에 연 1회 시드와 갱신 알림을 함께 뒀습니다.

공휴일마다 마감 문의가 반복됐습니다 — 달력일 -2가 범인이었습니다 그림 2


이중화 서버에서 캐시를 걷어낸 이유

공휴일 테이블은 작고 거의 안 바뀌는 참조 데이터라, 처음엔 당연히 인스턴스 메모리에 캐시할 생각이었습니다. 그런데 앱 인스턴스가 이중화되어 있다는 점이 걸렸습니다. 두 인스턴스가 각자 캐시를 들고 있으면, 공휴일을 하나 추가했을 때 한쪽만 갱신되고 다른 쪽은 stale 상태로 남을 수 있습니다.

같은 요청이 어느 서버에 떨어지느냐에 따라 마감이 달라지는 것은 최악의 버그입니다. 재현이 어렵고 사용자 문의로만 드러납니다. 그래서 캐시를 아예 걷어내고 매 호출 실시간 DB 조회로 갔습니다. 공휴일 테이블은 조회가 가벼워 실시간이 충분히 감당됩니다. 두 인스턴스가 같은 DB를 보니 어긋날 것이 없습니다.

물론 트래픽이 커지면 실시간 조회도 DB 부담이 됩니다. 그때는 짧은 TTL 캐시나 인스턴스 간 공유 캐시로 옮기면 됩니다. 지금 규모에서는 정합의 확실함이 캐시의 이득보다 컸습니다. 캐시는 공짜가 아니었습니다.

공휴일마다 마감 문의가 반복됐습니다 — 달력일 -2가 범인이었습니다 그림 3


같은 공휴일, 서비스마다 다른 정책

흥미로운 점은 같은 "공휴일"이라도 두 서비스의 취급이 갈렸다는 것입니다. 라운지는 공휴일에 아예 문을 열지 않으므로 예약 목록과 생성에서 그날을 통째로 제외합니다. 테이크아웃은 공휴일에도 발주하는 지점이 있어 그날 예약을 허용하되, 마감 계산에만 영업일 역산을 적용합니다.

공휴일이라는 개념은 하나의 전역 테이블로 두고, 그 개념을 어떻게 쓸지는 서비스별 분기로 구현했습니다. 개념과 정책을 분리해두니 새 서비스가 붙어도 공휴일 데이터는 재사용하고 정책만 정하면 됩니다.

이번 전환이 수월했던 배경에는 날짜·시간 처리를 한 곳으로 모아둔 이전 작업이 있습니다. 마감 계산이 화면과 API마다 제각각 흩어져 있었다면 영업일 역산으로 바꾸는 일은 수십 곳을 고치는 대공사가 됐을 것입니다. 시간 처리를 한 곳으로 모으는 계열의 앞선 이야기는 아래 글에 있습니다.


점검해볼 것들

예약·취소·정산 마감이 달력일 -N으로 계산되고 있지 않은지

공휴일·대체공휴일이 코드 하드코딩인지, 갱신 가능한 데이터인지

멀티 인스턴스 환경에서 인스턴스별 캐시가 서로 다른 답을 낼 수 있는지

같은 "공휴일"이 서비스마다 다른 정책을 요구하지 않는지

공휴일 데이터의 연간 갱신 담당자와 알림이 정해져 있는지

전환 후 공휴일이 낀 주의 마감 문의가 사라졌습니다. 이중화된 두 인스턴스는 항상 같은 답을 냅니다. 남은 일은 매년 공휴일 테이블을 채우는 것뿐이고, 그 알림은 이미 달려 있습니다.

#영업일계산#공휴일#마감역산#멀티인스턴스#캐시