근태관리 시스템
레거시 보안 시스템과의 boundary 분리, explicit handler 기반 도메인 자동화, 부분 데이터 위에서의 idempotency를 결정 기준으로 잡은 사내 근태관리 v1
기술 스택
개요
기존 출입통제·지문 단말기 시스템(MSSQL에 출퇴근 원본 로그를 적재하는 보안 솔루션) 위에, 사용자·관리자가 직접 사용할 수 있는 사내 근태관리 서비스를 새로 얹은 v1 프로젝트입니다. 출근 계획(WorkPlan)·실제 출퇴근(WorkLog)·지각/조퇴 집계(TardyLog / LeftEarlyLog)·재택 근무를 한 도메인 모델 위에서 다루도록 설계해, 보안 시스템에만 머물러 있던 출퇴근 데이터를 사내 운영 도구로 끌어올리는 것이 목표였습니다.
레거시 MSSQL이 read-only 시스템이며 Django 측 PostgreSQL과 따로 살아야 한다는 점, 단말기 데이터가 항상 완전하지 않다는 점(지문 누락·새벽 퇴근·반차) 두 가지를 전제로 — 두 DB를 끌어안는 ORM 통합 대신 단방향 boundary API, 시그널 자동화 대신 결정적 explicit handler, 부분 데이터 위에서도 깨지지 않는 idempotent 갱신 세 축을 골라 v1을 만들었습니다.
기술 스택
- Backend: Python, Django, PostgreSQL, MSSQL (pyodbc), NGINX
- Infra: Docker, Docker Compose, Naver Cloud Platform (NCP)
역할
PM 1·FE 1·BE 2 팀에서 백엔드 2인 중 한 명. v1(2024.02–2024.05) 한 사이클의 이종 DB 연계, 출퇴근 도메인 모델, 출근계획 생성, 지각/조퇴/재택 처리, NCP 배포까지 백엔드 핵심 도메인을 담당했습니다. 이후 v2 고도화에는 참여하지 않았으며, 이 글은 v1 범위(origin/main)의 설계 결정에 한정합니다.
주요 기여
-
레거시 MSSQL을 ORM에 끌어오지 않고 단방향 boundary API로 분리.
- 두 DB를 하나의 ORM에 묶는 대안 대신, MSSQL의 출퇴근 원본과 Django 도메인 모델 사이에 명시적인 입력 경로를 둔 boundary API로 구성했습니다. 크론이 MSSQL을 읽어 CSV로 떨구면 Django의 단일 REST 엔드포인트가 받아, 사용자 매칭·근무 유형 필터·시간 검증 같은 도메인 의미를 그 경계에서 모두 해석한 뒤 PostgreSQL에 적재합니다. MSSQL은 보안 시스템의 원본이라 Django가 절대 쓰지 않아야 하고, 모든 외부 데이터가 단일 경로를 통과하므로 검증·필터가 한 곳에 모이며, MSSQL이 멈춰도 Django는 기존 데이터로 계속 운영됩니다.
-
시그널 자동화 대신 명시적인 핸들러로 파생 로그를 만든 결정.
- 출퇴근 기록 저장 시그널로 지각·조퇴 로그를 자동 생성할 수도 있지만, 업로드 엔드포인트 안에서 명시적으로 호출하는 방식을 택했습니다. 파생 로그는 실제 출퇴근 데이터가 도착했을 때만 만들어져야 하고, 지문 누락·정정·부분 데이터 세 상태를 한 비교 로직에서 다뤄야 하며, 결정적으로 오늘 날짜의 데이터로는 판단을 보류해 같은 날 오전 9시에 사람을 지각으로 잘못 마크하지 않게 해야 했기 때문입니다.
-
부분 데이터·시간 경계·외부 의존성 실패를 한 묶음으로 다루는 운영 정합성 정책.
- 단말기는 새벽 0~6시 퇴근을 다음 날로 기록하고, 반차는 도착 시각에 따라 점심 포함 여부가 달라지며, 공공 공휴일 API는 가끔 응답을 못 주고, 크론은 늦거나 같은 날 다시 돌 수 있습니다. 새벽 퇴근은 전일 시프트로 합산하고, 반차 퇴근시각은 도착 시각 기준으로 점심 1시간 분기, 공휴일 API 실패 시에는 빈 결과로 조용히 진행해 누락된 공휴일은 사후 보정 가능한 “일반 근무”로 잡았습니다 — 시스템 전체를 멈추는 것보다 부분 적재가 낫다는 판단. 크론은 어제·오늘 두 날짜를 같이 처리하면서 존재 체크로 시계 skew나 재실행 어느 쪽에서도 같은 결과가 나오게 묶었습니다.
트러블슈팅
-
같은 날 진행 중 데이터로 사람이 “조퇴”로 잘못 마크되는 false-positive.
- 문제: 초기에는 단말기 데이터가 들어올 때마다 즉시 지각·조퇴 로그를 만들었습니다. 단말기는 출근·퇴근을 별도 이벤트로 기록하기 때문에, 같은 날 오전에 출근만 찍히고 퇴근은 아직 안 찍힌 사람이 그 시점의 데이터로 보면 “퇴근 없음 = 조퇴”로 보여 일괄 “조퇴”로 잘못 마크되는 케이스가 발생했습니다. 본질은 “아직 진행 중인 날의 부분 데이터로 결론을 내고 있다”는 점이었습니다.
- 해결: 비교 로직에
work_date == today이면 판단을 보류하는 분기를 추가해 진행 중 날짜의 데이터로는 파생 로그를 만들지 않도록 고정했고, 이후 동일한 false-positive가 재발하지 않게 됐습니다.
-
CSV 다회 업로드 시 같은 날 데이터가 잘못 갱신되던 동일 입력 안전성 결함.
- 문제: 단말기 CSV는 오전·오후·야간으로 하루 여러 번 올라오고 네트워크 재시도까지 겹치면 같은 날치가 짧은 간격에 반복 도달합니다. 초기 크론은 지각 보상 로그를 만들 때 조회 키에 실제 출근 시각까지 포함시켰는데, 두 번째 업로드에서 출근 시각이 살짝 다른 값으로 들어오면 같은 날 같은 사용자에 대해 새 행이 추가로 생성되거나 기존 행의 상태가 잘못 바뀌어 결과가 비결정적이었습니다.
- 해결: 조회 키를 출근계획 한 가지로 좁혀 “(사용자, 날짜)당 지각 로그는 최대 1건”이라는 동일 입력 안전성을 데이터 계층에서 강제했습니다. 누적 지각 카운터도 단순 증가 대신 DB의 원자 갱신 식으로 바꿔 동시 업로드 race까지 함께 흡수했습니다 — CSV가 몇 번 들어와도 결과가 같아지는 구조로 정착시켰습니다.
의의
레거시 보안 시스템과 새 도메인 모델 사이의 boundary를 흐리지 않으면서 출퇴근 데이터를 사내 서비스로 끌어올린 사례입니다. “DB를 합치지 않고 API로 잇는다”, “자동화는 가능하지만 결정적으로 해야 한다”, “부분 데이터에서도 깨지지 않는다” 세 가지를 v1의 결정 기준으로 삼고, 그 위에 idempotency·시간 경계·외부 API 실패 같은 디테일을 풀어낸 것이 가장 만족스러웠습니다.