데이터와 리포트 설계 시리즈 4/8
리포트 생성 작업을 job으로 모델링했다면 다음 질문이 생긴다.
누가 실제 파일을 만들 것인가?
API 요청을 받은 서버가 직접 만들 수도 있지만, 오래 걸리는 작업은 worker로 분리하는 편이 좋다.
API는 작업을 접수하고, worker는 파일을 만든다. 결과 파일은 object storage에 저장한다.
API와 worker를 나누는 이유
API의 역할은 사용자의 요청을 받는 것이다.
worker의 역할은 시간이 오래 걸리는 처리를 수행하는 것이다.
둘을 나누면 장점이 있다.
- API 응답이 빨라진다.
- 무거운 파일 생성이 일반 요청을 막지 않는다.
- worker 실패를 job 상태로 기록할 수 있다.
- 파일 생성 작업의 timeout과 메모리를 별도로 조정할 수 있다.
- 재시도와 모니터링이 쉬워진다.
API와 worker를 같은 코드베이스에 둘 수도 있다.
중요한 것은 실행 흐름과 책임을 나누는 것이다.
기본 흐름
전체 흐름은 다음과 같다.

사용자는 즉시 파일을 받지 않는다.
먼저 job id를 받고, 이후 상태를 조회한다.
worker 실행 방식
worker는 여러 방식으로 실행할 수 있다.
- Lambda 비동기 호출
- queue 메시지 소비
- background process
- cron 기반 batch
- container worker
작은 시스템에서는 Lambda 비동기 호출로 시작할 수 있다.
예를 들어 API Lambda가 job을 생성한 뒤 같은 Lambda를 비동기로 다시 호출하거나, 별도 worker Lambda를 호출할 수 있다.
중요한 것은 사용자의 HTTP 요청과 파일 생성 실행을 분리하는 것이다.
self-invoke 패턴
작은 Lambda 기반 시스템에서는 self-invoke 패턴도 가능하다.
API 요청을 처리하던 Lambda가 자기 자신을 비동기 호출하고, payload에 action과 job id를 넣는다.
개념적으로는 다음과 같다.
def handler(event, context):
if event.get("action") == "process_report_job":
return process_report_job(event["jobId"])
job_id = create_job(event)
invoke_async({
"action": "process_report_job",
"jobId": job_id
})
return {"jobId": job_id}
이 방식은 별도 worker 배포 단위를 만들지 않아도 된다는 장점이 있다.
하지만 코드가 커지면 API handler와 worker handler를 분리하는 것이 더 명확할 수 있다.
Object Storage에 파일을 두는 이유
생성된 파일은 object storage에 저장하는 것이 좋다.
예를 들어 S3 같은 저장소다.
장점은 다음과 같다.
- Lambda나 서버의 로컬 디스크에 의존하지 않는다.
- 사용자가 나중에 다시 다운로드할 수 있다.
- 파일 URL을 job 상태와 함께 저장할 수 있다.
- lifecycle 정책으로 오래된 파일을 정리할 수 있다.
- 여러 서버에서 같은 파일에 접근할 수 있다.
로컬 파일 시스템은 임시 작업 공간으로만 쓰고, 최종 결과는 object storage에 올리는 편이 안전하다.
파일 경로 설계
파일 경로도 설계 대상이다.
예시:
report-downloads-bucket/
└── reports/2026/05/18/job-12345/report.xlsx
경로에는 다음 정보를 담을 수 있다.
- 생성 연도
- 생성 월
- 생성 일
- job id
- 파일 이름
이렇게 하면 나중에 파일을 찾기 쉽다.
단, 사용자 식별 정보나 내부 업무명이 파일 경로에 그대로 들어가지 않도록 조심해야 한다.
완료 처리 순서
worker는 파일 업로드가 끝난 뒤 job을 완료 처리해야 한다.
순서가 중요하다.
1. job 상태 IN_PROGRESS
2. 데이터 조회
3. 파일 생성
4. object storage 업로드
5. 업로드 성공 확인
6. job 상태 COMPLETE + file URL 저장
파일 업로드 전에 COMPLETE로 바꾸면 사용자가 아직 없는 파일을 다운로드하려 할 수 있다.
반대로 파일 업로드는 되었는데 job 상태 갱신이 실패하면 orphan file이 생길 수 있다.
이런 예외도 운영 문서에 남겨야 한다.
실패 처리
worker가 실패하면 job을 FAILED로 바꾼다.
가능하면 실패 사유도 남긴다.
status = FAILED
error_message = "Object storage upload failed"
failed_at = now()
상세 stack trace는 로그에 남기고, job table에는 운영자가 이해할 수 있는 요약을 저장한다.
실패한 job을 자동 재시도할지, 사용자가 다시 요청하게 할지는 시스템 성격에 따라 결정한다.
다운로드 URL
완료된 job에는 다운로드 URL이 필요하다.
URL은 두 가지 방식이 있다.
- 공개 또는 인증된 정적 URL
- 만료 시간이 있는 signed URL
내부 운영 콘솔이라도 파일에 민감한 데이터가 있다면 접근 제어를 고려해야 한다.
URL을 job table에 저장할 때는 만료 정책도 함께 생각한다.
파일이 영구 보관되어도 되는지, 일정 기간 후 삭제해야 하는지 정해야 한다.
정리
리포트 생성에서 API와 worker를 나누면 구조가 안정된다.
API는 요청을 접수하고 job id를 반환한다.
worker는 데이터를 조회하고 파일을 만들고 object storage에 업로드한다.
job table은 두 흐름을 연결한다.
결과 파일은 서버 로컬이 아니라 object storage에 둔다.
이렇게 하면 오래 걸리는 리포트 생성도 추적 가능하고, 재다운로드 가능하며, 실패 대응이 쉬운 작업으로 바뀐다.
함께 볼 GitHub 저장소
'성장과 기술 > 시스템 설계' 카테고리의 다른 글
| Job table로 리포트 작업 상태 관리하기 (0) | 2026.06.02 |
|---|---|
| 오래 걸리는 엑셀 생성을 동기 API로 처리하면 생기는 문제 (0) | 2026.06.01 |
| 운영 콘솔의 리포트 기능은 왜 생각보다 어려운가 (0) | 2026.05.31 |
| 문서가 개인 브랜딩이 되는 순간 (0) | 2026.05.30 |
| 내부 문서를 블로그 글로 바꾸기 위한 익명화 체크리스트 (0) | 2026.05.29 |
글에서 정리한 생각은 GitHub의 코드와 포트폴리오로 이어지고, 일부는 FamBlend 같은 제품 실험으로 확장됩니다.