내 아키텍처를 재고하게 만든 마감일: Timesheetflow를 Domain Thinking으로 구축하기

발행: (2026년 1월 4일 오후 10:58 GMT+9)
10 min read
원문: Dev.to

Source: Dev.to

그 한 가지 질문이 처음에 **“그저 엑셀 처리”**라고 생각했던 방식을 바꾸어 놓았습니다.
저는 Timesheetflow를 만들고 있었습니다—여전히 엑셀 파일로 제출되는 월간 직원 근태표를 자동화하는 작은 시스템입니다 (현실적으로 많은 팀이 아직도 스프레드시트에 의존하고 있기 때문입니다).

실제 워크플로우 (비즈니스가 실제로 하는 일)

Timesheetflow의 워크플로우는 간단하고—매우 현실적입니다:

  1. 관리자가 처리 마감일을 설정합니다 (보통 월말).
  2. 직원들은 마감일 이전에 Google Drive 폴더에 자신의 월간 타임시트를 업로드합니다 (제출은 현재 월에만 해당).
  3. 마감일에 시스템이 타임시트를 하나씩 처리합니다.
  4. 승인자가 하나, 여러 개 또는 모든 타임시트를 검토하고 승인합니다.
    • 늦은 제출도 허용되지만 Late 라고 명확히 표시됩니다.
  5. 관리자는 해당 월의 Excel 급여 요약을 다운로드합니다.

제가 이렇게 적어보니 한 가지가 떠올랐습니다:

이것은 “Excel 자동화” 문제가 아닙니다.
이것은 월간 급여 마감 문제입니다.

What I Thought the Problem Was

My first approach was very implementation‑driven:

  • “There’s a Drive folder.”
  • “There are Excel files.”
  • “I need to parse and export.”

So my architecture naturally became a pipeline:

  • Drive client downloads files
  • A parser extracts rows
  • A validator checks columns
  • An exporter builds a consolidated report

It looked clean… but the code kept reading like:

“update tables, move files, generate output”

instead of:

“close the month, enforce the deadline, approve submissions, finalize payroll”

That difference sounds subtle, but it changes everything—especially when requirements evolve.

내가 나중에 깨달은 점

좋은 아키텍처는 단순히 레이어와 패턴에 관한 것이 아니다.
그것은 의미에 관한 것이다.

월말 워크플로를 도메인(배치 작업이 아니라)으로 다루기 시작한 순간, 설계가 더 명확해졌다:

  • 마감 기한은 타임스탬프 필드가 아니라—시스템 동작을 변경하는 규칙이다.
  • “Late”(지연)는 경고 라벨이 아니라—승인 가시성 및 감사 가능성에 영향을 미친다.
  • “Latest submission wins”(가장 최근 제출이 승리)는 쿼리 트릭이 아니라—비즈니스가 의존하는 정책이다.

내가 사용하기 시작한 보편적인 언어

파일이 아니라 비즈니스 관점으로 생각하기 시작하니, 용어가 명확해졌습니다:

용어의미
월간 실행 / 급여 기간마감되는 월
마감일마감 시간
근무시간표 제출한 직원의 월별 입력
지연 제출허용되지만 플래그 지정
최신 제출이 우선직원이 여러 버전을 업로드할 때의 정책
승인승인/거부; 일괄 승인 유효
급여 요약 내보내기관리자 전달물

코드가 이 언어를 사용한다면, 무엇을 하는지 설명하는 긴 주석이 필요하지 않습니다.

Before vs. After (Pipeline Thinking vs. Domain Thinking)

Before: “Process files”

// Pipeline mindset: process whatever is in the folder
var files = drive.ListXlsxFiles(folderId);

foreach (var file in files)
{
    var rows = ParseExcel(file);
    Validate(rows);
    Save(rows);
}

ExportSalarySummary();

It works—but it hides the real rules:

  • 이번 달은 어느 달인가?
  • 마감 기한이 강제되는가?
  • 직원이 파일을 두 번 업로드하면 어느 파일이 선택되는가?
  • 승인 후에는 무엇이 일어나는가?

After: “Close a monthly run”

// Domain mindset: close a payroll period with explicit rules
run.LockAtDeadline(now);

run.RefreshSubmissionsFromDrive();   // latest submission wins
run.FlagLateSubmissions();           // uploaded after deadline

foreach (var submission in run.ActiveSubmissions())
{
    submission.ValidateCurrentMonthOnly();
}

approvals.ApproveAllValid(run);      // late is a badge, invalid blocks approval

var report = payroll.ExportSalarySummary(run); // Excel for admin

Now the code reads like the business process:

  • 기간을 잠금
  • 알려진 규칙에 따라 제출물을 평가
  • 유효한 것만 승인
  • 급여 요약을 내보냄

그 변화 덕분에 모든 것을 더 쉽게 유지하고 설명할 수 있게 되었습니다.

핵심 도메인 규칙 (Excel보다 중요한)

  1. Current month only – Employees can submit timesheets for the active month only.
    현재 월만 – 직원은 현재 월에만 근무표를 제출할 수 있습니다.

  2. Deadline locks the run – At the deadline, the run transitions into a “locked” phase for processing.
    마감 시 실행이 잠김 – 마감 시점에 실행이 “잠긴” 단계로 전환되어 처리됩니다.

  3. Late submissions are allowed, but flagged – After the deadline, submissions are marked Late and stay visible in approval/export.
    지연 제출 허용, 단 플래그 표시 – 마감 후 제출은 Late 로 표시되며 승인/내보내기에서 계속 보입니다.

  4. Latest submission wins – If an employee uploads multiple timesheets for the same month:

    • the system uses the most recent file (by Drive timestamps)
    • older ones are marked superseded
      가장 최신 제출이 우선 – 직원이 같은 월에 여러 근무표를 업로드할 경우:
    • 시스템은 가장 최신 파일(Drive 타임스탬프 기준)을 사용합니다.
    • 이전 파일은 superseded 로 표시됩니다.
  5. Invalid beats late – If a timesheet is invalid, it’s invalid—whether it’s late or not.

    • Late is a badge.
    • Invalid is a blocker.
      Invalid가 Late보다 우선 – 근무표가 유효하지 않으면, 늦었든 아니든 무효입니다.
    • Late는 배지입니다.
    • Invalid는 차단 요인입니다.
  6. Salary export is the admin deliverable – The end result is not “processed rows”; it’s a monthly Excel summary that payroll/admin can use.
    Salary export는 관리자가 제공해야 할 결과물 – 최종 결과는 “처리된 행”이 아니라 급여/관리자가 사용할 수 있는 월간 Excel 요약입니다.

구현 노트 (기술 스택)

Timesheetflow는 다음으로 구축되었습니다:

  • ASP.NET Core (.NET 8) – 웹 + API
  • Hangfire – 마감 일정 및 백그라운드 처리
  • ClosedXML – Excel 파싱/내보내기
  • Google Drive – 제출 채널 (워크스페이스 폴더 + 서비스 계정 액세스)

간단한 스택이라도 도메인을 명확히 모델링함으로써 시스템이 “스크립트‑같은” 것이 아니라 “제품‑같은” 느낌을 주었습니다.

주요 내용

  • 이것은 실제로 Excel 문제라기보다 월말 급여 마감 문제였습니다.
  • 마감일, 지연 플래그, 그리고 **“가장 최근 제출이 승리”**는 도메인 규칙이며, 단순한 유틸리티 로직이 아닙니다.
  • 이러한 규칙을 중심으로 보편적인 언어를 구축하면 비즈니스 프로세스처럼 읽히는 코드를 만들 수 있어 유지보수와 향후 변경이 훨씬 쉬워집니다.

When Code Mirrors the Business Language

  • 코드가 비즈니스 언어를 반영하면 읽기, 테스트, 확장이 더 쉬워집니다.
  • 최고의 아키텍처는 단순히 실행되는 것이 아니라 의도를 전달합니다.

마무리 생각

Timesheetflow를 시작했을 때 나는 엑셀 자동화 도구를 만드는 것이라고 생각했습니다.
실제 가치는 파일 중심으로 설계하는 것을 멈추고, 매월 비즈니스가 따르는 워크플로우를 중심으로 설계하기 시작했을 때 찾아왔습니다.

“간단한 자동화”가 실제로는 도메인 문제였던 순간을 겪어본 적이 있다면, 이야기를 듣고 싶습니다.

나와 연결하기

  • GitHub:
  • LinkedIn:

원하신다면, 다음을 공유해 드릴 수 있습니다:

  • .NET 8용 최소 도메인 모델(MonthlyRun, TimesheetSubmission, Approval)
  • 마감일 + 처리 파이프라인을 위한 Hangfire 작업 레이아웃
  • Salary_Summary.xlsx 예시 구조(ByEmployee / Details / Exceptions)

이 글에 컴파일 가능한 코드(전체 샘플 클래스)를 포함시키길 원하시는지, 아니면 이 게시물처럼 개념적인 스니펫으로 유지하길 원하시는지 알려 주세요.

Back to Blog

관련 글

더 보기 »

Go에서 우아한 도메인 주도 설계 객체

❓ Go에서 도메인 객체를 어떻게 정의하시나요? Go는 전형적인 객체‑지향 언어가 아닙니다. Domain‑Driven Design(DDD) 같은 개념을 구현하려고 할 때, 예를 들어 En…

분리를 분리하자

소개 2025년 말 며칠 동안, 우리 팀 리더는 하루를 추가로 쉬면서 중요한 회의를 놓쳤습니다. 최근 구조조정 이후, 한 동료가 떠났습니다.