Code › system-design

DAU 10만 규모의 Document AI 시스템 아키텍처 설계하기

Document AI 시스템을 예시로 대규모 비동기 처리 아키텍처와 GPU 워커 설계를 정리

최근 Document AI 시스템 아키텍처를 공부하면서, 대규모 문서 분석 파이프라인을 어떻게 설계할 수 있을지 정리해 보았다.

Document AI는 일반적인 웹 서비스와 다르게 이미지 전처리, OCR, 레이아웃 분석, 개인정보 마스킹, LLM 처리 같은 무거운 작업들이 함께 들어간다. 특히 OCR이나 LLM 추론은 CPU만으로 처리하기 어렵고, GPU 리소스를 별도로 고려해야 한다.

이번 글에서는 DAU 10만 명 규모를 가정하고, 문서 업로드 요청이 들어왔을 때 데이터가 어떤 흐름으로 처리되는지 중심으로 정리해 보려고 한다.


1. 요구사항 정의

시스템 디자인을 할 때는 먼저 기능 요구사항과 비기능 요구사항을 나누어 정리하는 것이 좋다. 그래야 이후에 어떤 컴포넌트가 필요한지, 어떤 부분을 더 중요하게 설계해야 하는지 기준을 잡을 수 있다.

기능 요구사항

이 시스템에서 필요한 기능 요구사항은 다음과 같이 정리할 수 있다.

- PDF, 이미지 등 다양한 포맷의 문서 업로드
- 업로드된 문서에서 텍스트 추출
- 문서의 레이아웃, 표, 문단 구조 분석
- 추출된 데이터를 Markdown 또는 JSON 같은 구조화된 형태로 변환
- 변환된 데이터를 기반으로 한 자연어 질의응답(RAG) 기능
- 문서 처리 상태 조회

단순히 OCR로 텍스트만 뽑는 것이 아니라, 문서의 구조를 함께 이해하고 이후 검색이나 질의응답에 활용할 수 있는 형태로 가공하는 것이 핵심이다.

비기능 요구사항

비기능 요구사항은 시스템이 어떤 품질 기준을 만족해야 하는지에 가깝다.

- 정확성(Accuracy): 금융, 법률, 계약서 같은 문서에서도 높은 OCR 및 레이아웃 인식률 필요
- 지연 시간(Latency): 짧은 문서는 빠르게 처리하고, 대용량 문서는 비동기 처리
- 처리량(Throughput): Peak TPS 기준으로 안정적인 문서 처리
- 확장성(Scalability): 요청량 증가에 따라 API 서버와 워커를 독립적으로 확장
- 가용성(Availability): 일부 워커나 인스턴스 장애가 발생해도 전체 서비스는 계속 동작
- 신뢰성(Reliability): 작업 실패 시 재시도 및 DLQ 기반 재처리
- 보안(Security): 개인정보(PII) 마스킹 및 외부 API 전달 범위 통제

특히 Document AI 시스템에서는 정확성과 처리량을 동시에 고려해야 한다. 문서 분석 품질이 낮으면 결과를 신뢰하기 어렵고, 반대로 품질이 높더라도 처리 시간이 지나치게 길면 실제 서비스에서 사용하기 어렵다.

또한 OCR과 LLM 처리는 상대적으로 무거운 작업이기 때문에 모든 요청을 동기적으로 처리하기보다는, 문서 크기나 작업 종류에 따라 처리 방식을 나누는 것이 필요하다.

예를 들어 짧은 영수증이나 단일 이미지 문서는 빠른 처리를 목표로 하고, 수백 페이지짜리 PDF는 큐에 넣고 비동기로 처리하는 방식이 더 적절하다.

정확도와 Human-in-the-loop

여기서 정확성(Accuracy)은 단순히 OCR 모델의 전체 평균 정확도만 의미하지 않는다. 특히 보험금 지급, 금융 심사, 법률 문서 처리처럼 실제 금전이나 의사결정에 영향을 주는 시스템에서는 95% 수준의 정확도를 보이더라도 나머지 4~5%의 오류를 그대로 감수하기 어렵다.

실제 오류는 단순한 오타뿐만 아니라 금액, 날짜, 이름, 병원명, 진단명, 항목 매핑 같은 핵심 필드에서 발생할 수 있다.

- 금액 인식 오류
  10,000원 → 100,000원

- 날짜 인식 오류
  진료일, 청구일, 발급일 혼동

- 필드 매핑 오류
  총 진료비, 본인부담금, 청구금액을 잘못 연결

- 표 구조 인식 오류
  행과 열이 밀려 다른 항목의 금액으로 인식

- 손글씨 또는 병원별 양식 차이로 인한 인식 오류

따라서 이런 도메인에서는 Document AI 결과를 무조건 자동 처리하기보다는, 필드 단위의 신뢰도 점수(Confidence Score)와 룰 기반 검증을 함께 사용해야 한다.

예를 들어 금액 필드는 일반 텍스트보다 더 높은 기준을 적용할 수 있다.

금액 confidence >= 0.98
환자명 / 생년월일 매칭 성공
청구 금액이 약관 또는 지급 한도 내에 있음
총액과 세부 항목 합계가 일치함

→ 자동 처리 후보

반대로 하나라도 기준을 만족하지 못하면 사람 검수 큐(Human Review Queue)로 보내는 것이 안전하다.

Confidence 낮음
필드 간 값 불일치
금액이 비정상적으로 큼
원본 이미지 품질이 낮음
중요 필드가 누락됨

→ Human Review Queue

즉, 높은 OCR 정확도는 전체 업무를 완전히 무인화한다는 의미라기보다, 사람이 직접 입력하거나 검수해야 하는 작업량을 크게 줄여준다는 의미에 가깝다. 특히 금전 지급과 연결되는 시스템에서는 자동화와 사람 검수를 함께 사용하는 Human-in-the-loop 구조가 필요하다.


2. 용량 산정

시스템 디자인을 할 때 가장 먼저 해야 하는 일 중 하나는 트래픽 규모를 대략적으로 계산하는 것이다.

여기서는 다음과 같이 가정했다.

DAU: 100,000명
유저당 하루 평균 문서 업로드 횟수: 3회

하루 동안 발생하는 총 문서 업로드 요청 수는 다음과 같다.

100,000 × 3 = 300,000건

하루는 86,400초이므로 평균 TPS는 다음과 같이 계산할 수 있다.

평균 TPS = 300,000 / 86,400
        ≈ 3.47 TPS

평균 TPS만 보면 그렇게 큰 수치처럼 보이지 않는다. 하지만 실제 서비스에서는 트래픽이 하루 종일 균등하게 들어오지 않는다. 특정 시간대에 요청이 몰릴 수 있기 때문에 Peak TPS도 함께 고려해야 한다.

여기서는 단순하게 평균 TPS의 5배를 피크 트래픽으로 가정했다.

Peak TPS = 3.47 × 5
         ≈ 17.35 TPS

대략 20 TPS

일반적인 CRUD API 기준으로 20 TPS는 큰 수치가 아닐 수 있다. 하지만 문서 이미지 한 장을 처리하는 데 수백 ms에서 수 초 이상이 걸리는 AI 추론 시스템에서는 이야기가 달라진다.

요청 하나가 단순한 DB 조회가 아니라 OCR, 레이아웃 분석, LLM 처리까지 포함한다면 20 TPS도 꽤 무거운 부하가 될 수 있다. 그래서 이런 시스템에서는 API 서버와 AI 추론 워커를 분리하고, 중간에 큐를 두는 비동기 구조가 필요하다.

다만 여기서 계산한 TPS는 문서 업로드 요청 기준이다. 실제 GPU 워커의 처리량을 산정하려면 문서당 평균 페이지 수, 페이지당 OCR 처리 시간, LLM 호출 횟수 같은 값을 추가로 고려해야 한다.


3. 전체 아키텍처 흐름

Document AI 시스템의 전체 흐름을 단순화하면 다음과 같다.

[User]


┌──────────────────────────────────────────────┐
│  1. 진입 및 라우팅 레이어                       │
│     Web Server / API Gateway / Load Balancer │
└──────────────────────────────────────────────┘


┌──────────────────────────────────────────────┐
│  2. API Server                               │
│     파일 검증, 메타데이터 생성, 작업 요청 등록     │
└──────────────────────────────────────────────┘

   │  원본 파일 저장

┌──────────────────────────────────────────────┐
│  3. Object Storage                           │
│     S3 등                                     │
└──────────────────────────────────────────────┘

   │  작업 메시지 발행

┌──────────────────────────────────────────────┐
│  4. Message Queue                            │
│     SQS 등                                    │
└──────────────────────────────────────────────┘

   │  워커가 메시지 소비

┌──────────────────────────────────────────────┐
│  5. GPU Worker                               │
│     전처리 → OCR → 마스킹 → LLM 처리            │
└──────────────────────────────────────────────┘


┌──────────────────────────────────────────────┐
│  6. Storage Layer                            │
│     RDB / Vector DB / Object Storage         │
└──────────────────────────────────────────────┘

핵심은 사용자의 요청을 API 서버에서 끝까지 직접 처리하지 않는다는 점이다. API 서버는 문서를 접수하고, 파일을 저장하고, 작업 메시지를 큐에 넣은 뒤 빠르게 응답한다. 실제 무거운 AI 처리는 뒤쪽의 워커들이 비동기로 수행한다.


4. 진입 및 라우팅 레이어

사용자가 문서를 업로드하면 요청은 가장 먼저 서비스의 앞단 레이어를 통과한다. 이 레이어는 실제 문서 분석을 수행하는 곳은 아니지만, 외부 요청을 안전하게 받고 적절한 API 서버로 전달하는 역할을 한다.

단순화하면 다음과 같은 흐름으로 볼 수 있다.

Client


WAF / CDN


API Gateway 또는 Load Balancer


API Server

여기서 중요한 개념 중 하나는 Reverse Proxy다. Reverse Proxy는 클라이언트의 요청을 직접 받아 내부 서버로 대신 전달해 주는 서버다. 클라이언트 입장에서는 Reverse Proxy가 실제 서버처럼 보이지만, 실제로는 그 뒤에 여러 대의 API 서버가 있을 수 있다.

Client


Reverse Proxy

  ├── API Server 1
  ├── API Server 2
  └── API Server 3

Nginx나 Apache는 이런 Reverse Proxy 역할을 할 수 있는 대표적인 Web Server다. 정적 파일 서빙, TLS 종료, 요청 압축, 업로드 파일 크기 제한, 경로별 라우팅 같은 작업을 처리할 수 있다. 다만 AWS 같은 클라우드 환경에서는 ALB, API Gateway, CloudFront 같은 관리형 서비스가 많은 역할을 대신할 수 있기 때문에 Nginx를 반드시 직접 운영해야 하는 것은 아니다.

API Gateway는 API 관리에 더 초점이 맞춰져 있다. 인증/인가 연동, API Key 관리, Rate Limiting, 요청 검증, API 버전 관리, 로깅 같은 기능을 제공한다. 외부에 공개되는 API가 많고, 사용자별 호출 제한이나 요청 검증이 중요하다면 API Gateway를 사용하는 구성이 적합하다.

반면 Load Balancer는 여러 대의 서버로 트래픽을 분산하는 역할에 가깝다. AWS에서는 보통 ALB(Application Load Balancer)를 사용하며, Health Check를 통해 장애가 발생한 서버를 제외하고 정상 서버로만 요청을 전달할 수 있다.

ALB

  ├── API Server 1
  ├── API Server 2
  └── API Server 3

API Gateway와 ALB는 둘 다 앞단에서 요청을 받을 수 있지만 목적이 조금 다르다.

API Gateway
- API 관리 중심
- 인증/인가, Rate Limiting, API Key 관리에 유리
- 외부 공개 API 관리에 적합
- 상대적으로 비용과 지연 시간이 더 붙을 수 있음

ALB
- 트래픽 분산 중심
- 여러 서버로 요청을 나누는 데 적합
- Health Check를 통해 장애 서버 제외 가능
- 일반적인 웹/API 서버 앞단에 적합

이 Document AI 시스템에서는 문서 분석 자체를 앞단 레이어에서 처리하지 않는다. 앞단 레이어는 요청을 검증하고, 필요한 제한을 적용하고, 정상 요청만 API 서버까지 전달하는 역할에 집중한다.

AWS 기준으로는 외부 공개 API 관리, 사용자별 Rate Limit, API Key, 요청 검증이 중요하다면 API Gateway를 앞에 두는 구성이 자연스럽다. 반대로 단순한 웹/API 트래픽 분산과 서버 Health Check가 중심이라면 ALB만으로도 충분할 수 있다.

이 글에서는 다음과 같은 구성을 기본으로 생각했다.

Client


API Gateway 또는 ALB


API Server


S3 / SQS

정리하면, 진입 및 라우팅 레이어의 핵심은 다음과 같다.

- 외부 요청을 안전하게 받는다.
- 인증/인가와 요청 제한을 적용한다.
- 정상 요청만 내부 API 서버로 전달한다.
- 여러 API 서버로 트래픽을 분산한다.
- 장애가 발생한 서버로는 요청을 보내지 않는다.

즉, 이 레이어는 직접 문서를 분석하는 곳이 아니라, 시스템의 입구에서 요청을 정리하고 보호하는 역할을 한다.


5. API 서버 레이어

요청이 API 서버에 도착하면 먼저 파일과 요청 정보를 검증한다.

예를 들면 다음과 같은 검증이 필요하다.

- 지원하는 파일 형식인지 확인
- 파일 크기 제한 확인
- 사용자 권한 확인
- 요청 횟수 제한 확인
- 문서 처리 작업 메타데이터 생성

여기서 중요한 점은 API 서버가 직접 OCR이나 LLM 처리를 수행하지 않는다는 것이다.

문서 처리는 시간이 오래 걸릴 수 있기 때문에, API 서버가 동기적으로 끝까지 기다리면 요청 처리 시간이 길어지고 서버 리소스도 빠르게 소모된다. 그래서 API 서버는 원본 파일을 S3 같은 Object Storage에 저장하고, 큐에 작업 메시지를 넣은 뒤 사용자에게 작업 ID를 반환한다.

1. 파일 검증
2. 원본 파일을 S3에 저장
3. 작업 상태를 DB에 PENDING으로 저장
4. SQS에 작업 메시지 발행
5. 사용자에게 taskId 반환

사용자는 이후 이 taskId를 이용해 문서 처리 상태를 조회할 수 있다.


6. Message Queue 레이어와 멱등성

API 서버가 발행한 작업 메시지는 SQS 같은 Message Queue에 쌓인다.

큐를 사용하는 이유는 API 서버와 AI 워커를 분리하기 위해서다.

API Server → Queue → Worker

이 구조를 사용하면 순간적으로 요청이 몰리더라도 API 서버가 바로 워커를 호출하지 않고, 큐가 중간에서 작업량을 완충해 준다. 워커는 자신이 처리할 수 있는 속도에 맞춰 큐에서 메시지를 가져가면 된다.

또한 워커 수를 늘리면 큐에 쌓인 작업을 더 빠르게 처리할 수 있다.

큐가 많이 쌓임 → GPU Worker 수 증가
큐가 줄어듦   → GPU Worker 수 감소

다만 SQS는 기본적으로 at-least-once delivery 방식이기 때문에 같은 메시지가 중복 처리될 가능성을 완전히 배제할 수는 없다. 즉, 큐를 쓴다고 해서 메시지가 반드시 한 번만 처리된다고 가정하면 안 된다.

이 때문에 워커 로직은 멱등성(Idempotency)을 만족하도록 설계해야 한다.

멱등성이란 같은 요청이나 작업을 여러 번 수행해도 최종 결과가 한 번 수행한 것과 동일하게 유지되는 성질을 말한다.

예를 들어 같은 taskId를 가진 메시지가 두 번 전달되더라도 결과가 두 번 저장되거나, 사용자에게 알림이 두 번 발송되거나, 상태가 잘못 변경되면 안 된다.

같은 메시지가 두 번 도착한 경우

1번째 처리 → 결과 저장
2번째 처리 → 이미 완료된 작업이므로 재처리하지 않고 종료

이를 위해 다음과 같은 설계가 필요하다.

- 모든 문서 처리 작업에 고유한 taskId 부여
- taskId에 Unique Constraint 설정
- 작업 상태를 PENDING, PROCESSING, COMPLETED, FAILED 등으로 관리
- 워커가 작업을 시작하기 전에 현재 상태 확인
- 이미 COMPLETED 상태인 작업은 다시 처리하지 않음
- 결과 저장 경로를 taskId 기준으로 고정
- 알림 발송 같은 부수 효과는 sent_at 같은 필드로 중복 방지

예를 들어 워커가 메시지를 받았을 때 바로 처리하지 않고, 먼저 DB에서 작업 상태를 확인한다.

1. taskId로 작업 조회
2. status가 COMPLETED이면 처리하지 않고 종료
3. status가 PENDING 또는 retry 가능한 FAILED이면 PROCESSING으로 변경
4. OCR / LLM 처리 수행
5. 결과 저장
6. status를 COMPLETED로 변경
7. SQS 메시지 삭제

여기서 중요한 점은 상태 변경도 안전하게 처리해야 한다는 것이다. 여러 워커가 같은 메시지를 동시에 처리하려고 할 수 있기 때문에, 단순히 조회 후 업데이트하는 방식은 경쟁 조건이 생길 수 있다.

그래서 상태 변경은 조건부 업데이트 방식으로 처리하는 것이 좋다.

UPDATE document_tasks
SET status = 'PROCESSING'
WHERE id = :taskId
  AND status IN ('PENDING', 'FAILED');

이 업데이트가 성공한 워커만 실제 처리를 진행한다. 업데이트된 row가 없다면 이미 다른 워커가 처리 중이거나 완료한 작업이므로 현재 워커는 작업을 중단하면 된다.

결과 저장도 멱등하게 만들어야 한다. 예를 들어 결과 파일을 매번 새로운 경로에 append하는 방식으로 저장하면 중복 처리 시 결과가 여러 개 생길 수 있다. 대신 taskId를 기준으로 결과 경로를 고정하면 같은 작업이 다시 실행되더라도 같은 위치를 덮어쓰거나, 이미 존재하는 결과를 확인하고 종료할 수 있다.

s3://document-results/{taskId}/result.json

정리하면 Message Queue를 사용하는 비동기 구조에서는 중복 메시지와 재시도가 자연스럽게 발생할 수 있다. 따라서 “한 번만 처리된다”는 가정보다는, “여러 번 처리되어도 최종 상태가 깨지지 않는다”는 방향으로 설계해야 한다.


7. GPU Worker 레이어

실제 문서 처리는 GPU Worker에서 수행된다.

GPU Worker는 큐에서 메시지를 가져온 뒤, S3에서 원본 파일을 읽고 문서 처리 파이프라인을 실행한다.

SQS Message


원본 파일 다운로드


이미지 전처리


OCR / Layout Analysis


PII Masking


LLM 처리


결과 저장

다만 모든 단계가 반드시 GPU를 필요로 하는 것은 아니다. 이미지 회전 보정이나 간단한 개인정보 마스킹은 CPU로도 처리할 수 있다. GPU 리소스가 특히 중요한 구간은 OCR, 레이아웃 분석, LLM 추론처럼 딥러닝 모델이 직접 동작하는 부분이다.

이미지 전처리

먼저 문서 이미지의 품질을 높이기 위한 전처리를 수행한다.

예를 들어 다음과 같은 작업이 들어갈 수 있다.

- 이미지 회전 보정
- 노이즈 제거
- 해상도 조정
- 문서 영역 검출

문서가 기울어져 있거나 품질이 낮으면 OCR 결과도 나빠질 수 있기 때문에, 전처리는 전체 품질에 영향을 주는 단계다.

OCR 및 Layout Analysis

다음으로 OCR과 레이아웃 분석을 수행한다.

단순히 텍스트만 추출하는 것이 아니라 문서 안에서 제목, 본문, 표, 체크박스, 이미지 영역 등을 구분해야 할 수도 있다.

- 텍스트 추출
- 문단 구조 분석
- 표 영역 감지
- 좌표 정보 추출
- 페이지별 레이아웃 분석

Document AI에서는 이 단계가 특히 중요하다. 같은 텍스트라도 문서 안에서 어디에 위치해 있는지에 따라 의미가 달라질 수 있기 때문이다.

Confidence Scoring 및 Rule Validation

OCR과 레이아웃 분석이 끝났다고 해서 결과를 바로 최종 데이터로 저장하거나 지급 판단에 사용하면 위험할 수 있다. 특히 보험 청구나 금융 문서처럼 금액이 포함된 문서에서는 추출된 값이 얼마나 신뢰할 수 있는지 한 번 더 판단하는 단계가 필요하다.

이 단계에서는 OCR 결과에 대해 필드별 Confidence Score를 확인하고, 룰 기반 검증을 함께 수행한다.

OCR / Layout Analysis


Confidence Scoring


Rule Validation

   ├── 통과 → 다음 처리 단계로 진행
   └── 실패 → Human Review Queue

예를 들어 문서 전체의 평균 정확도가 높더라도, 실제로 중요한 것은 각 필드가 얼마나 정확하게 추출되었는지다.

병원명 confidence: 0.98
진료일 confidence: 0.96
환자명 confidence: 0.97
청구금액 confidence: 0.61

이 경우 병원명, 진료일, 환자명은 신뢰할 수 있더라도 청구금액은 지급 판단에 직접 영향을 주기 때문에 자동 처리하면 안 된다. 이런 문서는 사람 검수 대상으로 분류하는 것이 안전하다.

Rule Validation에서는 다음과 같은 검증을 수행할 수 있다.

- 필수 필드가 모두 존재하는지 확인
- 날짜 형식과 범위가 정상인지 확인
- 총액과 세부 항목 합계가 일치하는지 확인
- 청구 금액이 지급 한도 내에 있는지 확인
- 사용자 정보와 문서의 이름 / 생년월일이 일치하는지 확인
- 동일 문서가 중복 청구된 것은 아닌지 확인

검증을 통과한 문서만 자동 처리 대상으로 보내고, 검증에 실패하거나 신뢰도가 낮은 문서는 Human Review Queue로 보낸다.

자동 처리 가능
- 핵심 필드 confidence가 기준 이상
- 룰 검증 통과
- 금액이 정상 범위
- 중복 청구 아님

사람 검수 필요
- 핵심 필드 confidence가 낮음
- 금액 / 날짜 / 이름 불일치
- 표 구조 인식이 불안정함
- 원본 이미지 품질이 낮음

또한 원본 이미지, OCR 결과, 수정 전후 값, 검수자 정보, 처리 시간을 감사 로그(Audit Log)로 남겨야 한다. 나중에 잘못된 지급이나 분쟁이 발생했을 때 어떤 값이 자동 추출되었고, 어떤 기준으로 승인되었는지 추적할 수 있어야 하기 때문이다.

Audit Log
- documentId
- taskId
- originalValue
- extractedValue
- correctedValue
- confidenceScore
- validationResult
- reviewerId
- reviewedAt

정리하면, 금전 지급이나 심사와 연결되는 Document AI 시스템에서는 OCR 정확도가 높더라도 결과를 그대로 신뢰하면 안 된다. Confidence Scoring, Rule Validation, Human Review Queue, Audit Log를 함께 설계해야 실제 서비스에서 안전하게 사용할 수 있다.

개인정보 마스킹

OCR 결과를 LLM에 넘기기 전에 개인정보를 마스킹하는 단계도 필요하다.

문서에는 이름, 전화번호, 이메일, 주민등록번호, 주소 같은 민감한 정보가 포함될 수 있다. 특히 외부 LLM API를 사용할 경우, 원본 개인정보가 그대로 외부로 전달되지 않도록 주의해야 한다.

홍길동 → [NAME]
010-1234-5678 → [PHONE]
test@example.com → [EMAIL]

이 단계는 가능하면 내부 VPC 안에서 처리하는 것이 좋다. 정규식 기반 탐지와 NER 모델을 함께 사용할 수 있다.

다만 마스킹은 항상 완벽하지 않을 수 있다. 그래서 개인정보가 포함될 수 있는 문서를 다루는 시스템에서는 로깅, 저장, 외부 API 전송 범위까지 함께 설계해야 한다.

LLM 처리

마스킹이 끝난 텍스트는 LLM 처리 단계로 넘어간다.

LLM은 문서 요약, 핵심 정보 추출, JSON 구조화, 질의응답용 데이터 생성 등에 활용될 수 있다.

OCR 결과


마스킹된 텍스트


LLM


요약 / 구조화 / 검색용 데이터

LLM을 직접 서빙한다면 vLLM 같은 서빙 엔진을 사용할 수 있고, 외부 API를 사용할 수도 있다. 어떤 방식을 선택하느냐에 따라 비용, 지연 시간, 보안 요구사항이 달라진다.


8. 저장소 레이어

문서 처리가 끝나면 결과를 저장한다.

저장해야 하는 데이터는 크게 세 가지로 나눌 수 있다.

1. 원본 파일
2. 문서 처리 메타데이터
3. 추출 및 가공된 결과

원본 파일은 S3 같은 Object Storage에 저장한다.

작업 상태, 사용자 정보, 문서 ID, 처리 시간, 에러 상태 같은 메타데이터는 RDB에 저장할 수 있다.

documents
- id
- user_id
- file_url
- status
- created_at
- completed_at

OCR 결과나 LLM이 만든 구조화 데이터도 RDB나 Document DB에 저장할 수 있다.

또한 문서 검색이나 RAG 기능을 제공하려면 문서를 청크 단위로 나눈 뒤 임베딩을 생성하고 Vector DB에 저장해야 한다.

문서 텍스트


Chunking


Embedding


Vector DB

이렇게 해두면 사용자가 자연어로 질문했을 때 관련 문서 조각을 검색하고, 그 결과를 LLM에 함께 전달할 수 있다.


9. 저장소 선택과 트레이드오프

저장소를 설계할 때는 모든 데이터를 하나의 DB에 넣기보다, 데이터의 성격에 따라 저장 위치를 나누는 것이 좋다.

Document AI 시스템에서 다루는 데이터는 성격이 서로 다르다.

- 원본 파일
- 문서 메타데이터
- 작업 상태
- OCR 결과
- LLM 구조화 결과
- 검색용 임베딩

원본 파일은 크기가 크고 바이너리 데이터이기 때문에 RDB에 직접 저장하기보다 S3 같은 Object Storage에 저장하는 것이 적절하다. RDB에는 파일 자체가 아니라 파일의 위치와 메타데이터만 저장한다.

원본 파일 → Object Storage
파일 URL, 상태, 사용자 정보 → RDB

작업 상태와 메타데이터는 RDB가 잘 맞는다. 문서, 사용자, 작업 상태 사이의 관계가 명확하고, 상태 전이가 중요하기 때문이다.

예를 들어 문서 처리 작업은 다음과 같은 상태를 가진다.

PENDING → PROCESSING → COMPLETED
PENDING → PROCESSING → FAILED

이런 상태는 중복 처리 방지, 재시도, 사용자별 문서 조회와 연결되기 때문에 트랜잭션과 정합성이 중요하다. 그래서 PostgreSQL 같은 RDB를 기본 저장소로 두는 선택이 자연스럽다.

반면 OCR 결과나 LLM 결과는 문서마다 구조가 다를 수 있다. 어떤 문서는 표가 많고, 어떤 문서는 key-value 형태의 필드가 많고, 어떤 문서는 긴 본문 중심일 수 있다. 이런 데이터는 스키마가 고정되어 있지 않기 때문에 NoSQL이나 Document DB도 후보가 될 수 있다.

다만 초기 설계에서는 저장소를 지나치게 많이 나누면 운영 복잡도가 커진다. 그래서 PostgreSQL을 사용한다면 JSONB 컬럼에 유동적인 OCR/LLM 결과를 저장하는 방식도 가능하다.

선택지 1: PostgreSQL + JSONB
- 장점: RDB 안에서 메타데이터와 결과를 함께 관리 가능
- 장점: 운영 복잡도가 낮음
- 단점: 데이터 규모가 커지고 쿼리 패턴이 복잡해지면 한계가 생길 수 있음

선택지 2: Document DB / NoSQL
- 장점: 문서별로 다른 결과 구조를 저장하기 쉬움
- 장점: 스키마 변경에 유연함
- 단점: 트랜잭션과 관계형 쿼리는 RDB보다 불리할 수 있음

선택지 3: Vector DB
- 장점: RAG를 위한 유사도 검색에 적합
- 단점: 일반적인 메타데이터 조회나 상태 관리 용도로는 적합하지 않음

따라서 이 시스템에서는 다음과 같이 나누는 것이 합리적이라고 생각했다.

원본 파일: Object Storage
문서 메타데이터: RDB
작업 상태: RDB
OCR/LLM 결과: RDB JSONB 또는 Document DB
RAG 검색용 임베딩: Vector DB

초기에는 RDB와 Object Storage를 중심으로 단순하게 시작하고, RAG 검색 요구가 명확해지는 시점에 Vector DB를 추가하는 방식도 가능하다. 만약 PostgreSQL을 사용한다면 pgvector 같은 확장을 이용해 초기 Vector Search를 처리하는 선택지도 있다.

정리하면, 저장소 선택의 핵심은 “어떤 DB가 더 좋은가”가 아니라 “데이터의 성격과 쿼리 패턴에 맞게 어디에 저장할 것인가”에 있다.


10. 대용량 문서 처리와 Queue Starvation 문제

Document AI 시스템에서는 짧은 영수증 한 장만 처리하는 경우도 있지만, 수백 페이지짜리 PDF가 들어올 수도 있다.

이때 모든 요청을 하나의 큐와 하나의 워커 풀로 처리하면 Queue Starvation 문제가 발생할 수 있다.

Queue Starvation은 큐에 오래 걸리는 작업이 먼저 쌓이면서, 뒤에 들어온 가벼운 작업들이 계속 대기하게 되는 현상을 말한다.

예를 들어 500페이지짜리 PDF가 먼저 들어오면, 뒤에 들어온 1페이지짜리 문서가 오래 기다릴 수 있다.

[500페이지 PDF] [1페이지 영수증] [1페이지 계약서] ...

이 경우 1페이지 문서는 처리 자체는 금방 끝날 수 있지만, 앞에 있는 대용량 작업 때문에 큐에서 오랫동안 대기하게 된다. 사용자 입장에서는 간단한 문서 하나를 올렸는데도 결과가 늦게 나오는 문제가 생긴다.

이런 문제를 줄이기 위해 작업 종류에 따라 큐를 분리할 수 있다.

High Priority Queue
- 짧은 문서
- 실시간 응답이 중요한 요청
- 단일 이미지 또는 소량 페이지 문서

Low Priority Queue
- 대용량 PDF
- 배치성 작업
- 수십 페이지 이상 문서

그리고 워커도 큐별로 분리하거나, 우선순위에 따라 더 많은 워커를 배정할 수 있다.

High Priority Worker Pool
- 짧은 문서 처리 전용
- 낮은 지연 시간 목표

Low Priority Worker Pool
- 대용량 문서 처리
- 처리량 중심

예를 들어 전체 GPU 워커 중 일부는 항상 High Priority Queue만 처리하도록 고정해둘 수 있다.

GPU Worker Pool

High Priority 전용: 70%
Low Priority 전용: 30%

이렇게 하면 대용량 문서가 많이 들어오더라도 짧은 문서 요청이 계속 뒤로 밀리는 문제를 줄일 수 있다.

또 다른 방법은 하나의 큐를 사용하더라도 작업 크기에 따라 가중치 기반 스케줄링을 적용하는 것이다. 하지만 구현 복잡도가 올라가기 때문에, 시스템 디자인 단계에서는 우선순위 큐를 분리하는 방식이 가장 직관적이다.

정리하면, 대용량 문서 처리를 고려하는 Document AI 시스템에서는 단순히 “큐를 둔다”에서 끝나면 안 된다. 큐 안에서 작업 크기가 크게 다를 때 발생하는 Queue Starvation 문제까지 고려해야 한다.


11. 문서 청킹과 문맥 손실 문제

대용량 문서를 빠르게 처리하려면 페이지 단위로 나누어 여러 워커가 병렬로 처리하게 만들 수 있다.

하지만 이 방식에는 문맥 손실 문제가 있다.

예를 들어 어떤 문장이 페이지 끝에서 시작해서 다음 페이지로 이어지거나, 표가 여러 페이지에 걸쳐 이어지는 경우가 있다.

Page 1: 계약 기간은 2026년 1월 1일부터
Page 2: 2026년 12월 31일까지로 한다.

페이지를 완전히 독립적으로 처리하면 이런 연결 관계를 놓칠 수 있다.

그래서 OCR 자체는 페이지 단위로 처리하더라도, 후처리 단계에서는 앞뒤 페이지의 텍스트를 일부 겹치게 보는 방식이 필요할 수 있다.

Page 1 text + Page 2 일부
Page 2 text + Page 1 일부 + Page 3 일부
Page 3 text + Page 2 일부

이런 식의 overlap 또는 sliding window 방식으로 문맥을 보존하면, LLM이 문서를 해석할 때 앞뒤 흐름을 더 잘 이해할 수 있다.

표 같은 경우에는 단순 텍스트 청킹만으로는 부족할 수 있다. 표의 헤더, 행, 열 구조가 페이지를 넘어 이어질 수 있기 때문에, 표 구조를 별도로 보존하는 후처리 로직도 필요하다.


12. 장애와 재처리 설계

비동기 워커 구조에서는 실패 처리도 중요하다.

문서 처리 중에는 여러 단계에서 실패가 발생할 수 있다.

- 원본 파일 다운로드 실패
- OCR 처리 실패
- LLM API timeout
- 결과 저장 실패

이런 실패가 발생했을 때 단순히 작업을 버리면 안 된다. 재시도 가능한 에러인지, 재시도해도 의미 없는 에러인지 구분해야 한다.

예를 들어 일시적인 네트워크 오류나 LLM API timeout은 재시도할 수 있다. 반면 지원하지 않는 파일 형식이나 깨진 파일은 재시도해도 성공하기 어렵다.

Retry 가능한 에러
- 네트워크 오류
- 외부 API timeout
- 일시적인 DB 연결 실패

Retry해도 어려운 에러
- 깨진 파일
- 지원하지 않는 파일 형식
- 비밀번호가 걸린 PDF

SQS를 사용한다면 실패한 메시지를 일정 횟수 재시도한 뒤 DLQ(Dead Letter Queue)로 보낼 수 있다. DLQ에 쌓인 메시지는 별도로 모니터링하고, 필요하면 수동으로 재처리할 수 있다.

여기서도 멱등성 설계가 중요하다. 재시도는 같은 작업을 다시 실행한다는 뜻이고, 같은 작업이 다시 실행되어도 결과가 깨지지 않아야 하기 때문이다.

예를 들어 LLM 처리까지 완료했지만 최종 상태를 COMPLETED로 업데이트하기 직전에 워커가 죽을 수 있다. 이 경우 메시지는 다시 큐에 나타날 수 있고, 다른 워커가 같은 taskId를 다시 처리할 수 있다.

이때 결과 저장이 멱등하게 설계되어 있지 않으면 같은 결과가 중복 저장되거나, 사용자 알림이 여러 번 발송될 수 있다.

따라서 재처리 가능한 시스템을 만들려면 다음 기준을 함께 지켜야 한다.

- 작업의 고유 식별자(taskId)를 기준으로 처리
- 상태 전이를 조건부 업데이트로 관리
- 결과 저장 위치를 taskId 기준으로 고정
- 이미 완료된 작업은 no-op 처리
- 알림, 과금, 외부 API 호출 같은 부수 효과는 중복 실행 방지

결국 비동기 처리에서 중요한 것은 실패가 발생하지 않는 시스템을 만드는 것이 아니라, 실패가 발생해도 안전하게 다시 처리할 수 있는 구조를 만드는 것이다.


13. 마무리

Document AI 시스템은 단순히 OCR 모델이나 LLM을 붙인다고 끝나는 구조가 아니다.

문서 업로드 요청을 빠르게 접수하고, 무거운 AI 처리를 비동기로 분리하고, GPU 워커를 안정적으로 확장하고, 개인정보가 외부로 노출되지 않도록 처리하는 전체 흐름이 함께 설계되어야 한다.

특히 보험금 지급이나 금융 심사처럼 금전적인 의사결정과 연결되는 시스템에서는 OCR 정확도가 높더라도 결과를 그대로 자동 처리하면 안 된다. Confidence Scoring, Rule Validation, Human Review Queue, Audit Log를 함께 두어야 실제 서비스에서 안전하게 사용할 수 있다.

이번 정리를 통해 Document AI 시스템에서는 모델 자체의 성능뿐만 아니라 큐, 워커, 저장소, 재처리, 멱등성, 검수 흐름, 보안 경계 같은 백엔드 아키텍처 요소들이 매우 중요하다는 점을 다시 확인할 수 있었다.