들어가며
프로그래머로 살다 보면 "이거 내가 만들면 더 잘 만들겠는데" 싶은 순간이 있습니다. 연차 관리 시스템 DayOFF는 그 충동에서 시작됐습니다.
주변에서 연차 관리를 엑셀로 하는 걸 보면서 항상 불편함을 느꼈습니다. 수식이 잘못 설계되어 있어도 아무도 모르고, 누군가 실수로 셀을 건드리면 데이터가 날아가고, 매년 새해가 되면 파일을 새로 복사해서 써야 하는 상황. HR 담당자가 연차 계산 실수로 노동부에 신고 당하는 일도 심심치 않게 일어납니다.
"근로기준법대로 정확하게, 실무에서 바로 쓸 수 있는 연차관리 프로그램을 직접 만들자."
그렇게 DayOFF 개발이 시작됐습니다.

왜 직접 만들었나
시중 프로그램의 문제점
시중에 연차관리 프로그램이 없는 건 아닙니다. 상용 ERP 솔루션도 있고, 각종 엑셀 템플릿도 넘쳐납니다. 하지만 막상 써보면 문제가 한두 가지가 아닙니다.
❌ 상용 ERP 솔루션
→ 중소기업엔 지나치게 비쌈
→ 기능이 너무 많아서 오히려 불편
❌ 엑셀 템플릿
→ 계산식 오류가 많음
→ 관리자가 바뀌면 인수인계 어려움
→ 데이터 무결성 보장 안됨
❌ 무료 프로그램
→ 계산 기준이 불명확
→ 회계일 기준 미지원
→ 업데이트 없음
특히 연차 계산식이 문제였습니다. 노동부 공식 사이트인 노동OK 에서 직접 계산해보면 시중 프로그램들과 결과가 다른 경우가 빈번했습니다. 대부분의 프로그램이 그냥 입사 1년 후 15일 주고 끝내는 수준이었습니다.
회계일 기준 비례계산? 남은 월차 강행규정? 근속년수 보정? 이런 세부 사항을 제대로 구현한 프로그램을 찾기 어려웠습니다.
기술 스택 선택
UI 프레임워크: WPF 선택 이유
WinForms → 너무 올드한 UI, 커스터마이징 한계
Electron → 웹 기반이라 무겁고 배포 복잡
MAUI → 아직 성숙하지 않은 플랫폼
WPF → Windows 최강 UI, XAML 기반 자유도 높음
WPF를 선택한 핵심 이유는 XAML 기반의 높은 자유도입니다. 타이틀바부터 버튼 하나까지 원하는 대로 디자인할 수 있고, 애니메이션과 데이터 바인딩도 강력합니다. 실제로 기본 윈도우 캡션바를 완전히 제거하고 커스텀 타이틀바를 구현했습니다.
데이터베이스: SQLite + MariaDB 이중 구조
로컬 DB: SQLite (System.Data.SQLite.Core)
→ 직원 정보, 연차 내역, 설정 등 모든 업무 데이터
→ 인터넷 없이도 완전한 사용 가능
→ AppData\Roaming\DayOFF\DayOFF.db
서버 DB: MariaDB (PHP/Nginx on Ubuntu)
→ 라이센스 관리
→ 버전/업데이트 관리
→ 보안 정책 관리
업무 데이터는 절대 외부 서버에 보내지 않고 로컬에만 저장합니다. 서버는 오직 라이센스와 업데이트 관리에만 사용합니다.
SQLite vs JSON 데이터 관리 비교
왜 이주제를 다루나
연차관리 프로그램을 개발하면서 로컬 데이터 저장 방식을 고민했습니다. 실제로 시중에 배포된 특정 프로그램들을 뜯어보니
SQLite 대신 JSON 파일로 데이터를 관리하는 모습을 발견했습니다.
어떤 방식이 더 나을까요? 두 방식을 직접 비교해봤습니다.
JSON 방식
{
"employees": [
{
"id": "EMP001",
"name": "홍길동",
"department": "개발팀",
"hire_date": "2023-03-15",
"leaves": [
{
"start": "2024-01-02",
"end": "2024-01-02",
"count": 1.0,
"status": "승인"
}
]
}
]
}
JSON 장점
- 구현이 단순함 : Newtonsoft.Json 하나면 끝, 별도 DB 엔진 불필요
- 파일 하나로 관리 : 백업이 파일 복사로 끝남, 버전 관리에 친화적
- 사람이 읽을 수 있음 : 텍스트 에디터로 직접 수정 가능, 디버깅이 쉬움
- 이식성 : DB 드라이버 설치 불필요, 어떤 환경에서도 동작
JSON 단점
- 성능 문제 : 데이터 조회 시 파일 전체를 메모리에 로드, 직원 1000명 x 연차내역 10000건 = 대용량 JSON > 로드 시간 급증
- 동시성 문제 : 파일 잠금 없으면 동시 쓰기 시 데이터 손상, 멀티스레드 환경에서 위험
- 쿼리 불가능 : "이번달 연차 사용자 목록" 같은 조회 시 전체 로드 후 LINQ로 필터링해야 함 > 코드 복잡도 증가
- 트랜잭션 없음 : 저장 중 프로그램 강제 종료 시 파일 손상으로 데이터 전체 유실 가능
- 관계형 데이터 표현 어려움 : 직원-부서-연차내역 JOIN이 불가능, 중복 데이터 발생
- 인덱스 없음 : 데이터가 많아질수록 검색 속도 선형 하락
SQLite 방식
CREATE TABLE 직원 (
사원번호 TEXT PRIMARY KEY,
이름 TEXT NOT NULL,
부서 TEXT,
입사일 TEXT NOT NULL
);
CREATE TABLE 연차내역 (
ID INTEGER PRIMARY KEY AUTOINCREMENT,
사원번호 TEXT REFERENCES 직원(사원번호),
시작일 TEXT NOT NULL,
종료일 TEXT NOT NULL,
소진개수 REAL NOT NULL,
상태 TEXT NOT NULL
);
SQLite 장점
- SQL 쿼리 사용 가능 : 복잡한 조회도 한 줄로 해결, SELECT, JOIN, GROUP BY, ORDER BY 전부 지원
- 트랜잭션 보장 (ACID) : 저장 중 강제 종료돼도 데이터 안전, BEGIN TRANSACTION / ROLLBACK 지원
- 인덱스 지원 : 대용량 데이터도 빠른 검색, 사원번호, 날짜 컬럼 인덱스로 성능 최적화
- 관계형 데이터 표현 : FOREIGN KEY 로 데이터 무결성 보장, JOIN으로 복잡한 쿼리 단순화
- 파일 기반 (서버 불필요) : 단일 .db 파일로 관리, 설치/배포 간단
- 동시성 처리 : WAL(Write-Ahead Logging) 모드로 읽기/쓰기 동시 처리 가능
SQLite 단점
- 구현 복잡도 : 테이블 설계, 마이그레이션 필요, SQL 문법 학습 필요
- 의존성 : System.Data.SQLite 패키지 필요, x64/x86 SQLite.Interop.dll 배포 필요
- 사람이 직접 읽기 어려움 : 바이너리 파일이라 텍스트 에디터로 볼 수 없음, DB Browser for SQLite 같은 툴 필요
- 스키마 변경 어려움 : 컬럼 추가/삭제 시 마이그레이션 필요 ALTER TABLE 제약이 있음
실제 성능 비교
직원 500명, 연차5000건 기준
| 작업 | JSON | SQLite |
| 전체 로드 | 약 800ms | 약 50ms |
| 특정 직원 조회 | 약 200ms | 약 5ms |
| 이번달 연차자 조회 | 약 400ms | 약 10ms |
| 데이터 저장 | 약 600ms | 약 20ms |
| 파일 크기 | 약 8MB | 약 2MB |
데이터가 적을 때는 차이가 미미하지만 데이터가 쌓일수록 격차가 벌어집니다
어떤 걸 선택해야 할까
JSON이 적합한 경우
- 설정 파일 (config.json)
- 소량의 단순 데이터
- API 응답/요청 데이터
- 빠른 프로토타이핑
SQLite가 적합한 경우
- 관계형 데이터 (직원-부서-연차내역)
- 대용량 데이터 (수백~수천 건 이상)
- 복잡한 조회가 필요한 경우
- 데이터 무결성이 중요한 경우
- 동시 접근이 필요한 경우
DayOFF는 당연히 SQLite 를 선택했습니다. 직원-부서-연차내역-공제내역이 서로 연관된 관계형 데이터이고, 기간별 조회, 집계, 필터링이 빈번하게 필요하기 때문입니다.
연차 계산 엔진 구현
DayOFF의 핵심은 LeaveCalculator.cs 입니다. 근로기준법을 그대로 코드로 옮겼습니다.
기본구조

적용기간 목록 생성
연차 계산의 핵심은 기간을 어떻게 나누느냐입니다.
입사일 기준
입사일: 2024.03.15
1차: 2024.03.15 ~ 2025.03.14 (월차 기간)
2차: 2025.03.15 ~ 2026.03.14 (연차 15일)
3차: 2026.03.15 ~ 2027.03.14 (연차 15일)
4차: 2027.03.15 ~ 2028.03.14 (연차 16일)
회계일 기준
입사일: 2024.07.15
1차: 2024.07.15 ~ 2024.12.31 (월차 기간)
2차: 2025.01.01 ~ 2025.12.31 (비례 + 남은월차)
3차: 2026.01.01 ~ 2026.12.31 (연차 15일)


회계일 기준 비례계산
2017년 5월 30일 이후 입사자(신법 적용)가 회계일 기준을 사용할 때 첫 회계연도는 비례계산이 적용됩니다.
입사일: 2024.07.15
첫회계일: 2025.01.01
재직일수 = 2025.01.01 - 2024.07.15 = 170일
전체일수 = 2026.01.01 - 2025.01.01 = 365일
비례연차 = 15 × (170 ÷ 365) = 6.98일 → 소수점 처리 후 7일

소수점 처리
실무에서 HR 담당자가 "반올림해줘" 라고 하면 소수점을 없애고 정수로 떨어지게 해달라는 의미입니다.
7.4일 > 7일, 7.5일 > 8일

MidpointRounding.AwayFromZero 를 지정한 이유는 C# 기본 반올림이 은행가 반올림(Banker's Rounding)이라 2.5 → 2 로 처리되기 때문입니다. 실무에서는 2.5 > 3 이어야 합니다.
발견하고 수정한 버그들
개발하면서 타 프로그램에서 흔히 발생하는 버그들을 발견하고 전부 수정했습니다.
버그 1: 월차 증발 버그 (회계일 기준)
문제
회계일 기준에서 2차 기간(첫 회계연도)이 되면
기간정보.월차여부 = false 로 설정됨
→ 입사 후 1년이 안 됐는데도 월차 계산 안됨
→ 2차 기간에 걸쳐있는 월차가 증발!
예: 2024.07.15 입사 (회계월: 1월)
1차(7/15~12/31): 월차여부=true → 5일 계산 ✅
2차(1/1~12/31): 월차여부=false → 1~6월 월차 6일 증발! ❌
해결

근로기준법 제60조 제2항은 강행규정입니다. 동일기준, 사용자기준을 선택하더라도 월차 11일은 반드시 보장되어야 합니다.
버그 2: 무한 이월 버그
문제
// 기존 코드
for (int i = 1; i < 결과.Count; i++)
{
double 이전잔여 = 결과[i - 1].잔여일수;
if (이전잔여 > 0)
결과[i].이월일수 = 이전잔여; // 무조건 이월!
}
// 결과: 연차가 무한정 누적됨
해결
근로기준법상 연차는 발생 후 1년 내 미사용 시 소멸합니다. 자동 이월 로직 전체 제거 후 수동 이월 버튼으로만 처리합니다. 미사용 연차 소멸 시 연차수당 정산은 별도 연차수당내역에서 수동 처리합니다.
버그 3: 회계일 기준 근속년수 오계산
문제
입사일: 2022.03.01 (회계월: 1월)
5차 (2026.01.01):
기존 계산: 근속 = 2026 - 2022 = 4
03.01 > 01.01 → true → 근속-- → 3
발생 = 15 + (3-1)/2 = 16일
입사일 기준이었다면:
2026.03.01 = 5년차 시작
2026.01.01 시점엔 아직 4년차
→ 근속 = 3 → 16일 (같음)
하지만 특정 케이스에서 회계일 기준이
입사일 기준보다 연차가 적게 발생 가능
→ 근로기준법 위반!
해결

버그 4: 사용일수 비례배분 오류
문제
직원이 12/30(금) ~ 1/3(화) 연차 3일 사용
(12/31 토, 1/1 일 주말)
기존 코드 (물리적 일수 비례):
전체 5일, 1차겹침 2일, 2차겹침 3일
1차: 3 × (2/5) = 1.2일 ❌
2차: 3 × (3/5) = 1.8일 ❌
실제 정답:
전체근무일: 금,월,화 = 3일
1차근무일: 금 = 1일
2차근무일: 월,화 = 2일
1차: 3 × (1/3) = 1일 ✅
2차: 3 × (2/3) = 2일 ✅
해결

공휴일 정보는 구글 스프레드시트에서 매년 자동 로드됩니다.

버그 5: DateTime 시간값 오차
문제
DB에서 "2024-07-15" 를 DateTime.Parse 하면
내부적으로 "2024-07-15 00:00:00" 이지만
환경에 따라 시간값이 포함될 수 있음
날짜 뺄셈 시:
(2025.01.01 - 2024.07.15).TotalDays
→ 시간값 포함 시 170.9999... 또는 171.0001...
→ 비례계산 오차 발생
해결
모든 날짜 파싱에 .Date 적용 , 날짜 계산도 전부 .Date 통일
버그 6: Get_첫회계일 경계값 오류
문제
// 기존 코드
if (후보 <= 입사일) 후보 = 후보.AddYears(1);
// 문제:
// 입사일: 2024.01.01, 회계월: 1
// 후보 = 2024.01.01
// 후보 <= 입사일 → true (같은 날!)
// → 2025.01.01 로 밀려버림
// → 1차 월차 기간이 1년이 되어버림
해결
// <= → < 로 변경 (입사일 당일이 회계일이면 그날부터 시작)
if (후보 < 입사일.Date) 후보 = 후보.AddYears(1).Date;
다음 글 예고
지금까지 DayOFF의 탄생 배경부터 핵심 연차 계산 엔진, 그리고 개발하면서 발견하고 수정한 버그들까지 살펴봤습니다.
솔직히 처음엔 "연차 계산이 뭐가 어렵겠어" 싶었습니다. 막상 파고들어보니 근로기준법 하나하나가 코드의 분기점이 되고, 엣지케이스 하나하나가 버그로 이어졌습니다. 노동OK와 계산 결과를 맞추는 과정에서 수십 번의 수정과 테스트를 반복했고, 그 과정에서 타 프로그램에서 흔히 발생하는 계산 오류들을 전부 잡아낼 수 있었습니다.
정리하면 1편에서 다룬 내용은
- DayOFF 개발 배경과 기술 스택 선택 이유
- 연차 계산 엔진 구조와 구현 방식
- 입사일/회계일 기준 비례계산 로직
- 발견하고 수정한 대표적 버그
- SQLite vs JSON 데이터 관리 비교
2편 예고
다음 글에서는 DayOFF의 또 다른 핵심인 라이센스 시스템과 보안을 다룹니다.
- 라이센스 시스템 설계 : 16자리 키 + 4자리 인증코드 구조
- 보안 구현
- 관리자 웹 페이지
- 자동 업데이트 시스템
단순한 프로그램도 제대로 보호하려면 생각보다 많은 보안 기술이 필요합니다. 실제로 라이센스를 우회해보고 그걸 막는 과정을 담으려 합니다만 추후 배포할 생각이 있는 프로그램의 우회법을 알려주는게 아닌가 싶어 고민입니다, 담게 된다면 보안에 관심 있으신 분들께 도움이 될 것 같습니다.
2편에서 이어집니다
https://imoracle.tistory.com/79
[연차 관리 프로그램] DayOFF 개발기, 라이센스와 보안의 모든 것 -2-
들어가며지난 1편에서는 DayOFF의 탄생 배경과 연차 계산 엔진 구현, 그리고 개발하면서 발견한 버그들을 다뤘습니다. 1편 보러가기https://imoracle.tistory.com/78 [연차 관리 프로그램] DayOFF 개발기, 편
imoracle.tistory.com
'Project > [연차 관리 프로그램] DayOFF' 카테고리의 다른 글
| [연차 관리 프로그램] DayOFF 개발기, 완성 그리고 배포 -3- (0) | 2026.06.01 |
|---|---|
| [연차 관리 프로그램] DayOFF 개발기, 라이센스와 보안의 모든 것 -2- (0) | 2026.06.01 |