콘텐츠로 이동

출퇴근 관리 — 신호 하나로는 "출근했다"를 믿을 수 없다

출퇴근 체크 버튼 하나 만드는 게 뭐가 어렵겠나 싶었습니다. 그런데 “이 사람이 정말 회사에 있는가”를 증명하려는 순간, 한 가지 신호로는 어림없다는 걸 알게 됐습니다. 그리고 더 의외였던 건, 끝없는 버그의 근원이 보안이 아니라 시간(시각)을 저장하는 방식이었다는 사실입니다. 이 글은 출퇴근이라는 단순해 보이는 기능 뒤에 숨은 두 개의 깊은 결정을, 쉽게 풀어 쓴 기록입니다.

출퇴근 앱(SL-On) 홈 화면 — 큰 '출근' 버튼, 현재 시각 09:36, '오창 본사 위치 확인됨', 월별 통계·지각 기록·근무지 지도·위치 진단 버튼
실제 출퇴근 앱(SL-On)의 '출근' 화면. 버튼 하나를 누르는 순간 GPS·WiFi·QR·사업장이 동시에 검증되고, 그 결과가 아래 “오창 본사 위치 확인됨”으로 표시됩니다. 표준 09:00 대비 지각 36분도 즉시 계산됩니다.

1. 출퇴근이 왜 “그냥 버튼”이 아닌가

섹션 제목: “1. 출퇴근이 왜 “그냥 버튼”이 아닌가”

생산직과 사무직이 함께 일하는 제조 회사에서 출퇴근은 단순한 기록이 아닙니다. 그것은 근태 → 급여 → 연월차로 이어지는 데이터의 출발점입니다. 출근 한 번이 야근수당, 지각 차감, 휴가 잔여일까지 줄줄이 영향을 줍니다. 그래서 “정말 그 사람이, 정말 그 사업장에서” 찍었는지가 다른 어떤 기능보다 중요했습니다.

여기서 진짜 적은 “게으름”이 아니라 부정 출퇴근입니다. 집에서 원격으로 출근을 찍거나(원격), 동료에게 대신 찍어 달라고 하거나(대리), 위치를 위조하는(스푸핑) 경우입니다. 단일 신호로는 이걸 못 막습니다.

2. 신호 하나하나가 왜 부족한가

섹션 제목: “2. 신호 하나하나가 왜 부족한가”

각 신호를 따로 떼어 보면 전부 구멍이 있습니다.

신호혼자 쓰면 뚫리는 방식
GPS 위치만위치 위조 앱으로 좌표를 속일 수 있습니다. 실내에서는 정확도도 떨어집니다.
QR 코드만사업장 QR을 사진으로 찍어 동료에게 보내면 그 자리에 없어도 찍힙니다(대리 출근).
WiFi만같은 네트워크 이름을 흉내 내거나, 네트워크 정보만으로는 “진짜 그 자리”를 못 증명합니다.
사업장 선택만그냥 드롭다운에서 고르는 값 — 아무 증거가 없습니다.

각자 약합니다. 그런데 네 개가 동시에 맞아떨어지기는 매우 어렵습니다. 위치를 속이면서, 그 자리의 QR을 갖고 있으면서, 그 장소의 WiFi에 잡혀 있으면서, 올바른 사업장을 고르는 — 이 모든 걸 한꺼번에 위조하기는 비용이 너무 큽니다. 그래서 신호를 네 개 모으기로 했습니다.

이건 보안에서 흔히 쓰는 사고방식입니다. 자물쇠 하나는 따이기 쉽지만, 종류가 다른 자물쇠 네 개를 동시에 따려면 공격 비용이 급격히 올라갑니다. 각 신호의 약점이 서로 다르다는 점이 핵심입니다 — GPS는 위조에 약하지만 QR은 위조가 아니라 “공유”에 약하고, WiFi는 그 장소에 물리적으로 있어야 잡힙니다. 약점이 겹치지 않는 신호를 겹쳐 쌓으면, 한 방식으로 전부를 뚫을 수 없게 됩니다. 우리가 노린 건 “완벽한 한 개”가 아니라 “약점이 다른 여러 개의 합”이었습니다.

3. 그래서 신호를 네 개 모았다 (4중 신호 검증)

섹션 제목: “3. 그래서 신호를 네 개 모았다 (4중 신호 검증)”

체크인/아웃 시 한 가지가 아니라 네 가지 신호를 함께 수집해 서버로 보내 검증합니다.

네 신호가 모두 맞아야 체크인 통과 QR 토큰현장 동적 QR GPS 위치사업장 반경 내? WiFi 식별자그 장소 네트워크? 사업장 ID어느 사업장 서버 검증 모두 일치(AND)? ✓ 체크인 성공근태에 기록 ✗ 거부하나라도 어긋나면
4중 신호는 AND 조건입니다 — 하나라도 어긋나면 거부. 부정 출퇴근을 막는 것을 사용자 편의보다 우선한 결정이었습니다.

그래서 진단 화면은 “막혔다”가 아니라 “무엇이 막았다”를 보여줘야 합니다. 우리가 둔 점검 순서는 직관과 살짝 다릅니다.

점검 순서신호왜 이 순서인가
① 가장 먼저QR 토큰 상태만료·무효화된 토큰이 “조용히” 막는 경우가 가장 헷갈린다
WiFi 식별자실내에서 흔히 어긋난다
GPS 위치가장 의심하기 쉽지만 실제 원인인 경우는 의외로 적다
사업장 ID설정/선택 오류

핵심은 “눈에 잘 띄는 것(GPS)“이 아니라 “조용히 막는 것(QR 토큰)“을 가장 먼저 의심하도록 순서를 못 박은 것입니다. 디버깅에서 가장 비싼 건 잘못된 순서로 헤매는 시간입니다.

4. 진짜 끈질긴 버그는 “시간”이었다

섹션 제목: “4. 진짜 끈질긴 버그는 “시간”이었다”

의외로 가장 오래 괴롭힌 버그는 보안이 아니라 시각이었습니다. 출근은 오전 9시에 찍었는데 화면에는 오후 6시로 뜨고, 어떤 화면은 +9시간 어긋나고, 어떤 통계는 날짜 경계가 하루 밀립니다. 같은 데이터인데 보는 곳마다 시간이 달랐습니다.

출퇴근 앱 근태 화면 — 6월 캘린더에 정상·지각 표시, 최근 기록에 17:07→18:25(지각), 08:54→09:00(정상, 18:00 KST 자동 마감) 등 시각 기록
같은 근태를 캘린더·최근 기록으로 본 화면. 17:07 → 18:25, 08:54 → 09:00처럼 모든 시각이 KST로 일관되게 보입니다. 화면마다 9시간씩 어긋나던 문제를 끝낸 Fake-UTC 전략의 결과입니다.

교과서대로라면 서버는 UTC로 저장하고 화면에서 KST로 변환해 보여줘야 합니다. 그런데 출퇴근은 모바일·서버·화면이 전부 한국 시간으로만 동작합니다. 이런 환경에서 레이어마다 변환을 끼워 넣다 보면, 어디선가 변환을 빠뜨리거나 두 번 해서 시각이 어긋납니다. 변환 지점이 많을수록 버그의 틈도 많아집니다.

정통 UTC 저장 (변환 多) 모바일 KST 서버−9 저장 화면+9 표시 변환을 한 번이라도 빠뜨리면 → +9시간 버그 Fake-UTC (변환 0) 모바일 KST 서버그대로 저장 화면그대로 표시 핵심 아이디어 "값은 한국 시각 그대로, 라벨만 UTC라고 붙인다." → 모든 레이어가 같은 숫자를 변환 없이 본다 = 어긋날 곳이 없다
위(빨강)는 변환 단계가 많아 한 곳만 빠뜨려도 +9시간 버그. 아래(초록) Fake-UTC는 변환을 0으로 만들어 어긋날 틈을 없앱니다.

처방: “값은 KST, 라벨만 UTC” (Fake-UTC 전략)

섹션 제목: “처방: “값은 KST, 라벨만 UTC” (Fake-UTC 전략)”

그래서 근태에 한해 전략을 바꿨습니다. 한국 시각 값을 그대로 저장하되, 타임존 라벨만 UTC로 붙입니다. 읽을 때도 변환 없이 그대로 읽고, 화면에도 그대로 띄웁니다. 전 레이어가 같은 숫자를 보니 표시 오류가 사라졌습니다. 정석을 버린 대신, 일관성을 얻은 것입니다.

왜 정석(UTC 저장 후 변환)이 이론적으로 옳은데도 여기선 버렸을까요? 정석은 “여러 타임존의 사용자가 섞여 있을 때” 빛을 발합니다. 뉴욕과 서울 사용자가 같은 사건을 각자의 시간으로 봐야 한다면, 기준 시각(UTC)을 저장하고 각자 변환하는 게 맞습니다. 그런데 우리 근태는 모두가 한국에 있고, 한국 시간으로만 의미가 있습니다. 다양성이 없는 곳에 다양성을 위한 장치(변환)를 끼우면, 그 장치는 이득 없이 버그의 틈만 만듭니다. 설계는 “교과서가 옳다고 하는 것”이 아니라 “이 문제의 실제 모양”에 맞춰야 한다는 걸, 이 시각 버그가 비싸게 가르쳐 줬습니다.

대신 이 전략에는 분명한 그늘이 있습니다. 일반 타임스탬프와 의미가 달라 개발자가 헷갈리기 쉽고, 단일 타임존을 전제하므로 해외 사업장이 생기면 통째로 재설계해야 합니다. 트레이드오프를 모른 척하지 않고, 한계까지 함께 기록으로 남긴 이유입니다. 좋은 예외는 “왜 했는지”와 “언제 깨지는지”를 둘 다 적어 둔 예외입니다.

근태 정정도 그냥 고치게 두지 않았습니다. 사무직의 정정 신청은 전자결재 문서로 자동 생성되어 결재선을 타고, 승인되는 순간에만 근태에 반영됩니다. 누가·언제·무엇을·왜 고쳤는지가 결재 기록으로 영구히 남습니다. “별도의 정정 기능”이 아니라 “결재를 거친 사건”으로 만든 것입니다. 생산직은 더 가벼운 단일 승인 흐름으로 분기했습니다. 단일 백엔드 위에서 모듈이 한 데이터를 공유하기에 가능한 통합이었습니다.

이 출퇴근 모바일 앱은 현재 완성 단계로 본다는 것이 CEO의 판단입니다(1인 개발·미배포 단계).


이 글은 SL.AIMS를 만들며 겪은 현장 회고 중 하나입니다. 전체 그림은 〈사례연구: SL.AIMS〉에 있습니다.