서론: 왜 출석 체크 오류가 ‘자정’에 몰려 보일까

어두운 밤 사무실 배경에 12:00 AM 시계, 달력 체크와 경고, 서버로그를 담은 인포그래픽 모습이다

출석 체크가 잘 되다가도 특정 시간대, 특히 자정 전후에만 실패한다는 이야기는 커뮤니티나 서비스형 게시판에서 반복적으로 등장한다. 이용자 입장에서는 “내가 뭘 잘못 눌렀나”부터 시작해 “서버가 터진 건가”까지 추측이 넓게 퍼지는데, 실제로는 자정이라는 시간이 시스템적으로 ‘경계 조건’이 겹치는 구간이라 문제가 집중되는 경우가 많다. 하루가 바뀌는 순간에는 날짜 계산, 일일 제한 초기화, 배치 작업, 캐시 갱신, 통계 집계 같은 작업이 동시에 몰리기 쉽다, 더불어 출석 체크는 보통 단순한 버튼 클릭처럼 보이지만, 내부적으로는 중복 방지, 포인트 산정(비금전적 기여도), 세션 검증, 기록 저장을 한 번에 처리하는 경우가 많아 작은 지연에도 민감하다. 그래서 “자정에만”이라는 패턴은 사용자 체감이 과장된 게 아니라, 기술적으로도 충분히 그럴 만한 구조적 이유가 있다. 이 글은 자정 집중 오류가 생기는 대표적인 서버·DB·캐시·스케줄링 원인을, 사람들이 실제로 궁금해하는 확인 포인트 흐름에 맞춰 정리한다.

1) 자정은 ‘일일 상태’가 한꺼번에 바뀌는 경계 조건이다

날짜 계산과 타임존 불일치: 서버는 자정이 아닐 수도 있다

가장 먼저 확인되는 지점은 “자정”이 누구 기준의 자정인지다. 사용자는 로컬 시간(브라우저/모바일) 기준으로 자정을 인지다만, 서버는 UTC나 다른 타임존으로 움직이는 경우가 흔하다. 이때 출석 체크의 일자 판정이 서버 시간 기준이면, 사용자가 자정이라고 느끼는 시점과 서버의 날짜 변경 시점이 어긋날 수 있다. 더 복잡한 경우는 애플리케이션 서버는 KST, DB는 UTC, 배치 서버는 또 다른 타임존처럼 구성 요소마다 기준이 다를 때다. 그러면 같은 요청이라도 어느 컴포넌트가 날짜를 계산했는지에 따라 “오늘 출석”과 “어제 출석” 판정이 뒤섞인다, 특히 자정 전후 몇 분은 이 오차가 바로 사용자 오류로 보이기 때문에, 커뮤니티에서는 “0시 1분에 했는데 어제 처리됨” 같은 사례가 반복된다. 타임존 통일, DB 레벨에서의 날짜 계산 일원화, 그리고 로그에 타임존을 명시하는 방식이 핵심 점검 포인트로 남는다.

일일 제한 초기화 로직의 경쟁 상태: reset과 check가 동시에 달린다

출석 체크는 보통 “하루 1회” 같은 제약이 붙는데, 이 제약을 관리하는 상태값이 자정에 초기화된다. 문제는 초기화 작업(reset)과 출석 요청(check)이 같은 테이블/키를 건드릴 때 경쟁 상태가 발생할 수 있다는 점이다. 예를 들어 자정 00:00:00에 일괄 초기화 배치가 돌고, 동시에 수천 명이 출석 버튼을 누르면, 어떤 요청은 초기화 전 상태를 읽고, 어떤 요청은 초기화 후 상태를 읽는다. 더 나쁜 경우는 트랜잭션 격리 수준이나 락 처리 방식 때문에 “이미 출석 처리됨” 또는 “중복 요청” 같은 오류가 튀어나온다. 사용자 입장에서는 한 번 눌렀는데 실패했다고 보여 재시도하고, 재시도가 다시 충돌을 키우는 패턴도 흔하다. 이 구간은 단순히 서버 증설로 해결되지 않고, 초기화 방식을 “일괄 reset”에서 “날짜 기반 계산(오늘 날짜와 비교)”로 바꾸거나, 원자적 업서트(upsert)로 중복을 구조적으로 막는 방식이 재발을 줄인다. 결국 자정은 ‘상태가 바뀌는 순간’이기 때문에 잠깐의 비일관성이 커지기 쉽다.

2) 자정 배치 작업과 리소스 스파이크가 출석 API를 눌러버린다

통계 집계·정산·랭킹 갱신: “매일 0시에 돌리는 작업”의 부작용

많은 서비스가 하루 단위 통계 집계, 게시판 활동량 정리, 랭킹 계산, 로그 압축, 알림 큐 정리 같은 작업을 자정에 몰아서 실행한다. 운영 관점에서는 “하루가 끝났으니 0시에 정리”가 자연스럽지만, 기술적으로는 DB와 캐시에 큰 부하를 주는 선택이 되기 쉽다. 집계 쿼리가 대량 스캔을 유발하거나, 인덱스를 제대로 못 타서 I/O를 폭증시키면, 출석 체크처럼 짧은 트랜잭션도 대기열에 묶인다. 특히 출석 체크가 ‘기록 저장 + 사용자 상태 업데이트 + 포인트/기여도 계산 + 이벤트 로그 적재’를 한 번에 수행하면, 평소에는 견디던 DB가 자정 스파이크에서만 지연을 드러낸다. 이용자들이 “자정에만 느리다”라고 말하는 이유는, 그때만 눈에 띄게 타임아웃이 늘어나기 때문이다. 배치 작업의 분산(0시 집중을 피하는 스케줄링), 증분 집계, 별도 분석 DB로의 오프로딩 같은 접근이 자정 오류를 줄이는 정석으로 자주 언급된다.

로그 로테이션·백업·스토리지 작업: CPU보다 디스크가 먼저 막힌다

자정에는 애플리케이션 로그 로테이션, DB 백업, 스냅샷 생성, 오브젝트 스토리지 동기화 같은 운영 작업이 예약되는 경우가 많다. 이 작업들은 CPU나 메모리보다 디스크 I/O와 네트워크 대역폭을 크게 잡아먹는다. 출석 체크는 작은 요청이지만, 결국 DB 쓰기와 로그 기록을 포함하므로 디스크가 바빠지면 응답 지연이 순식간에 커진다. 특히 컨테이너 환경에서 동일 노드에 여러 서비스가 공존하면, 한쪽의 백업 작업이 다른 쪽의 API 레이턴시를 올리는 일이 생긴다. 사용자는 화면에서 “오류”만 보지만, 서버에서는 타임아웃이나 커넥션 풀 대기가 늘어난 흔적이 남는다. 이때 흔히 보이는 현상은 0시 전후로 5xx가 증가하고, DB 커넥션 수는 한계에 근접하며, 애플리케이션은 “connection acquisition timeout” 같은 로그를 뿜는 패턴이다. 결국 자정 오류는 애플리케이션 로직만의 문제가 아니라, 운영 스케줄이 만든 자원 경합일 가능성도 크다.

0시를 가리키는 디지털 시계와 넘어가는 달력, UI 패널 색이 동시에 바뀌는 장면이다

3) 캐시·세션·분산락이 자정에 흔들리는 이유

캐시 만료(Expiry) 폭탄과 키 동시 갱신: 미스가 한 번에 터진다

출석 여부를 빠르게 판단하려고 Redis 같은 캐시를 쓰는 경우가 많은데 키 만료 시간이 하루 끝과 맞물리면 자정에 캐시 미스가 동시다발로 발생할 수 있으며, 룰렛 휠의 착시 효과: 당신의 눈이 공의 위치를 오인하는 물리적 배경처럼 시스템 역시 정렬된 조건에서 순간적인 왜곡을 만들어냅니다. 예를 들어 오늘 출석 여부 키를 자정 만료로 걸어두면 0시에 키가 한꺼번에 사라지고 그 순간부터 모든 요청이 DB를 직접 조회하게 됩니다. 평소에는 캐시 히트로 가볍게 처리되던 트래픽이 갑자기 DB로 몰리면서 병목이 생기고, 이 병목이 다시 API 타임아웃으로 이어집니다. 이를 캐시 스탬피드라고 부르며, 해결은 만료 시간에 사용자별 랜덤 지터를 주거나 자정 정각 만료 같은 정렬된 정책을 피하고, 캐시 재생성 시 분산락을 신중히 적용하는 방식으로 정리됩니다.

세션 갱신·토큰 재발급과 인증 서버 병목: 출석이 아니라 인증이 막힌다

이용자들이 자정에 출석 체크를 시도할 때. “출석 api” 자체가 아니라 인증 계층에서 먼저 막히는 경우도 많다. 예컨대 세션이 ‘하루 단위’로 갱신되도록 설계되어 있거나, 토큰 만료가 자정에 맞춰지는 정책이면, 0시에 인증 갱신 요청이 폭주한다. 그러면 로그인 갱신이나 권한 확인이 느려지고, 출석 체크는 인증 실패나 401/403처럼 보이거나, 프론트에서는 단순 오류로 뭉개져 표시되기도 한다. 커뮤니티에서 “자정에만 로그인이 풀린다” “출석 누르면 다시 로그인하래” 같은 말이 같이 등장하면 이 흐름을 의심해 볼 만하다. 인증 서버가 별도로 분리되어 있어도, 결국 DB나 Redis 같은 공용 자원을 공유하면 병목은 연쇄적으로 전파된다. 이 경우 해결의 핵심은 토큰 만료를 특정 시각에 정렬하지 않고 분산시키거나, 갱신 경로의 캐시/레이트리밋을 정교하게 잡는 쪽으로 정리된다.

4) 데이터베이스 관점에서 자정 오류가 생기는 대표 패턴

유니크 제약·업서트 설계 미흡: “중복 출석”을 막다 실패한다

출석 체크는 중복 기록을 막기 위해 (user_id, date) 유니크 인덱스를 두는 경우가 많다. 이 자체는 좋은데, 애플리케이션이 “먼저 조회하고 없으면 insert” 같은 2단계 로직으로 구현되어 있으면 동시성에서 깨지기 쉽다. 자정에는 트래픽이 평소보다 몰리면서 동일 사용자의 중복 클릭, 네트워크 재전송, 앱의 재시도 로직이 겹치고, 그 결과 insert 충돌이 빈번해진다. 제대로 처리하면 “이미 출석”으로 정상 응답을 내려야 하지만, 예외 처리가 부족하면 500으로 떨어져 사용자는 오류로 인지한다. 또 날짜 경계에서 ‘어제/오늘’ 판정이 엇갈리면 실제로는 서로 다른 date로 들어가야 하는데, 애플리케이션이 로컬 날짜를 쓰는 바람에 유니크 충돌이 엉뚱하게 발생할 수도 있다. 그래서 자정 오류 분석에서 “DB 에러 로그에 duplicate key가 늘었는지”를 먼저 확인하는 흐름이 자주 관찰된다. 안전한 방식은 DB 원자 연산(INSERT … ON CONFLICT/ON DUPLICATE KEY UPDATE)으로 처리하고, 충돌을 오류가 아니라 상태 응답으로 변환하는 것이다.

파티셔닝·인덱스 경계와 느린 쿼리: 날짜가 바뀌는 순간 계획이 달라진다

출석 테이블이나 이벤트 로그 테이블이 날짜 기반 파티셔닝을 쓰는 경우, 자정에 새 파티션이 생성되거나 라우팅 규칙이 바뀐다. 준비가 잘 되어 있으면 문제 없지만, 파티션 자동 생성이 지연되거나, 새 파티션에 인덱스가 아직 없거나, 통계 정보가 갱신되지 않아 쿼리 플래너가 나쁜 실행 계획을 선택하면 자정에만 느려질 수 있다. 운영에서는 “0시 이후부터 갑자기 쿼리가 느려졌다가 안정된다” 같은 그래프가 남기도 한다. 또 일일 테이블 롤오버(예: attendance_20251214 같은 테이블 생성) 패턴을 쓰면, 테이블 생성/권한 부여/마이그레이션이 동시에 일어나며 잠깐의 락이 발생할 수 있다. 이 락은 짧아도 출석 API의 타임아웃 기준을 넘기면 사용자에게는 실패로 보인다. 결국 자정은 데이터 구조가 전환되는 시점이기도 해서, 파티션/인덱스/통계 갱신이 “미리” 준비되어 있는지가 중요해진다.

결론: 자정 집중 오류는 ‘한 가지 원인’보다 경계 조건과 동시 작업의 합성으로 생긴다

출석 체크 오류가 자정에만 몰리는 현상은 대개 우연이 아니라, 날짜 경계에서 상태가 바뀌고 작업이 겹치는 구조가 만들어낸 결과로 설명되는 경우가 많다, 타임존 불일치로 날짜 판정이 흔들리거나, 일일 초기화 배치와 출석 요청이 경쟁하면서 잠깐의 비일관성이 생길 수 있다. 여기에 통계 집계·백업·로그 로테이션 같은 자정 스케줄 작업이 DB와 스토리지 자원을 잡아먹으면, 평소에는 멀쩡하던 출석 API도 타임아웃과 에러를 내기 쉬워진다. 캐시 만료가 자정에 정렬되어 있으면 캐시 미스가 한꺼번에 터져 DB 부하를 증폭시키고, 인증 토큰 만료 정책까지 겹치면 “출석이 안 된다”는 체감은 더 커진다. 실제로는 출석 버튼 하나의 문제가 아니라, 자정이라는 경계에서 시스템 구성요소들이 동시에 방향을 바꾸는 순간의 합성 문제가 되는 셈이다. 그래서 원인 분석은 ‘자정 전후’ 로그와 지표를 묶어서 보고, 날짜 계산 기준·배치 스케줄·캐시 만료 정책·DB 동시성 처리까지 함께 점검하는 흐름으로 접근하는 편이 가장 현실적으로 맞아떨어진다.