데이터와 리포트 설계 시리즈 2/8
처음 리포트 다운로드 기능을 만들 때 가장 쉬운 방식은 동기 API다.
사용자가 버튼을 누르면 API가 데이터를 조회하고, 엑셀을 만들고, 바로 파일을 응답한다.
작은 데이터라면 이 방식도 괜찮다.
하지만 데이터 범위가 넓어지고 생성 시간이 길어지면 동기 API 방식은 여러 문제를 만든다.
가장 단순한 구조
동기 방식은 이렇게 생겼다.
사용자 클릭
-> POST /api/reports
-> DB 조회
-> 엑셀 생성
-> 파일 응답
구현은 단순하다.
프론트엔드도 요청 하나만 보내면 된다.
서버도 파일을 만들어 바로 응답하면 된다.
하지만 이 구조는 "작업이 짧게 끝난다"는 전제를 가진다.
그 전제가 깨지면 문제가 시작된다.
브라우저 대기 UX
동기 API는 사용자가 요청이 끝날 때까지 기다려야 한다.
리포트 생성이 3초 정도면 괜찮다.
하지만 20초, 40초, 1분 가까이 걸리면 사용자는 불안해진다.
- 버튼을 다시 누른다.
- 새로고침한다.
- 브라우저를 닫는다.
- 다운로드가 실패했다고 생각한다.
이 행동은 중복 작업을 만들 수 있다.
서버 입장에서는 같은 무거운 리포트를 여러 번 만들게 된다.
API timeout
많은 HTTP 계층에는 timeout이 있다.
API Gateway, load balancer, application server, browser, Lambda 모두 각각 제한이 있을 수 있다.
동기 API에서 작업이 timeout보다 오래 걸리면 사용자는 실패 응답을 받는다.
문제는 서버 작업이 실제로는 계속 진행 중일 수도 있다는 점이다.
사용자는 실패했다고 생각하고 다시 요청한다.
서버는 이미 진행 중인 작업과 새 작업을 동시에 처리할 수 있다.
Lambda timeout과 메모리 문제
Lambda로 엑셀을 생성한다면 timeout과 메모리도 고려해야 한다.
리포트 생성은 다음 자원을 쓴다.
- DB connection
- 메모리
- CPU
- 임시 디스크
- 네트워크
데이터가 커지면 엑셀 파일 생성 중 메모리가 커질 수 있다.
함수 timeout을 늘리는 것만으로는 근본 해결이 아닐 수 있다.
처리 모델 자체를 바꿔야 한다.
실패 상태가 남지 않는다
동기 API는 실패하면 보통 요청이 끝난다.
하지만 작업 상태가 별도로 저장되지 않으면 운영자는 무엇이 실패했는지 알기 어렵다.
예를 들어:
- 요청은 접수되었는가
- DB 조회 중 실패했는가
- 파일 생성 중 실패했는가
- 업로드 중 실패했는가
- 응답 전송 중 실패했는가
동기 API 하나로 처리하면 이 단계들이 한 요청 로그 안에 묻힌다.
나중에 추적하기 어렵다.
중복 요청 문제
사용자가 같은 조건으로 여러 번 요청할 수 있다.
동기 구조에서는 이를 막기 어렵다.
예를 들어 사용자가 버튼을 두 번 누르면 같은 리포트가 두 번 생성될 수 있다.
이것이 큰 문제가 아닐 수도 있지만, 파일 생성이 무겁거나 DB 부하가 크다면 문제가 된다.
job 단위로 관리하면 같은 조건의 작업이 이미 진행 중인지 확인할 수 있다.
동기 API에서는 이 관리가 자연스럽지 않다.
파일 전달 실패
엑셀 파일을 생성하는 것과 사용자가 다운로드하는 것은 다른 문제다.
동기 응답으로 파일을 바로 내려주면 네트워크 문제로 다운로드가 실패했을 때 재사용할 결과물이 없다.
반면 파일을 object storage에 저장해두면 사용자는 나중에 다시 다운로드할 수 있다.
운영자도 생성 이력을 확인할 수 있다.
리포트 파일은 응답 body로만 존재하기보다 저장 가능한 산출물로 다루는 편이 좋다.
해결 방향: job으로 모델링하기
오래 걸리는 리포트는 HTTP 요청이 아니라 job으로 모델링하는 편이 좋다.
흐름은 이렇게 바뀐다.
POST /api/reports
-> job 생성
-> worker 비동기 실행
-> 즉시 jobId 응답
GET /api/jobs/{jobId}
-> 상태 확인
-> 완료 시 다운로드 URL 반환
사용자는 파일이 아니라 job id를 먼저 받는다.
시스템은 작업 상태를 관리한다.
파일은 완료 후 object storage에 저장된다.
사용자 경험도 더 명확해진다
비동기 방식은 UX가 더 복잡해 보일 수 있다.
하지만 오래 걸리는 작업에서는 오히려 명확하다.
사용자에게 다음 상태를 보여줄 수 있다.
- 요청 접수됨
- 처리 중
- 완료
- 실패
- 다시 시도 가능
진행률까지 정확히 제공하지 못해도 상태만 있어도 충분히 낫다.
사용자는 브라우저 요청이 멈춘 것인지, 서버가 처리 중인지 구분할 수 있다.
언제 동기 API도 괜찮은가
동기 API가 항상 나쁜 것은 아니다.
다음 조건이면 괜찮다.
- 데이터가 작다.
- 항상 몇 초 안에 끝난다.
- 파일 생성이 가볍다.
- 실패해도 다시 요청하면 된다.
- 생성 이력을 남길 필요가 없다.
- 사용자 수가 매우 적다.
하지만 시간이 길어지거나 실패 추적이 필요해지는 순간 비동기 구조를 검토해야 한다.
정리
오래 걸리는 엑셀 생성을 동기 API로 처리하면 다음 문제가 생긴다.
- 브라우저 대기 UX
- timeout
- 중복 요청
- 실패 상태 추적 어려움
- 파일 재다운로드 어려움
- DB와 서버 부하
timeout을 늘리는 것은 임시 처방일 수 있다.
근본적으로는 작업을 job으로 모델링해야 한다.
사용자는 요청을 보내고 job id를 받는다.
worker는 백그라운드에서 파일을 만들고, 완료 후 다운로드 URL을 제공한다.
리포트 생성은 응답 시간이 긴 API가 아니라 비동기 작업이다.
함께 볼 GitHub 저장소
'성장과 기술 > 시스템 설계' 카테고리의 다른 글
| 운영 콘솔의 리포트 기능은 왜 생각보다 어려운가 (0) | 2026.05.31 |
|---|---|
| 문서가 개인 브랜딩이 되는 순간 (0) | 2026.05.30 |
| 내부 문서를 블로그 글로 바꾸기 위한 익명화 체크리스트 (0) | 2026.05.29 |
| 배포 가이드는 명령어 모음이 아니다 (0) | 2026.05.28 |
| 트러블슈팅 문서는 장애가 난 뒤 쓰면 늦다 (0) | 2026.05.27 |
글에서 정리한 생각은 GitHub의 코드와 포트폴리오로 이어지고, 일부는 FamBlend 같은 제품 실험으로 확장됩니다.