fos-blog/study
01 / 홈02 / 카테고리03 / 시리즈
01 / 홈02 / 카테고리03 / 시리즈

카테고리

  • AI 페이지로 이동
    • RAG 페이지로 이동
    • agent 페이지로 이동
    • langgraph 페이지로 이동
    • 사람용 CLI와 AI 에이전트용 CLI는 설계가 다르다
    • agents.md
    • BMAD Method — AI 에이전트로 애자일 개발하는 방법론
    • Claude Code 메모리: CLAUDE.md와 .claude/rules를 규칙으로 쓰는 법
    • Claude Code의 Skill 시스템 - 개발자를 위한 AI 자동화의 새로운 차원
    • Claude Code를 5주 더 쓴 결과 — 스킬·CLAUDE.md를 키워가는 방식
    • Claude Code를 11일 동안 쓴 결과 — 데이터로 본 나의 사용 패턴
    • Claude Code 멀티 에이전트 — Teams
    • AI 에이전트와 디자인의 새 컨벤션 — DESIGN.md, Google Stitch, Claude Design
    • Docling — IBM Research 의 문서 파싱 toolkit 상세 정리
    • 하네스 엔지니어링 실전 — 4인 에이전트 팀으로 코딩 파이프라인 구축하기
    • 하네스 엔지니어링 — 오래 실행되는 AI 에이전트를 위한 설계
    • 멀티모달 LLM (Multimodal Large Language Model)
    • AI 에이전트와 함께 MVP 만들기 — dooray-cli 사례
    • OpenClaw는 context와 memory를 어떻게 관리하나 — 나만의 에이전트를 구성하는 법
    • OpenClaw vs Hermes Agent — 갈아탈까 고민하며 정리한 비교
    • 스킬 문서를 신경망처럼 학습시킨다 — Microsoft SkillOpt 분석
  • ai 페이지로 이동
    • agent 페이지로 이동
    • [초안] AI 제품 백엔드 안정성 — 지연·비용·권한·관측·도구 실패·폴백/재시도/사람 에스컬레이션
    • [초안] LLM 평가 프레임워크: 골든셋, 회귀 테스트, LLM-as-a-judge, 사람 피드백 루프
  • algorithm 페이지로 이동
    • live-coding 페이지로 이동
    • 분산 계산을 위한 알고리즘
  • apartment 페이지로 이동
    • 구리 럭키아파트 24평 인테리어 레퍼런스 모음
  • architecture 페이지로 이동
    • [초안] 시니어 백엔드를 위한 API 설계 실전 스터디 팩 — REST · 멱등성 · 페이지네이션 · 버전 전략
    • [초안] API Versioning과 Backward Compatibility: 시니어 백엔드 관점 정리
    • 캐시 설계 전략 총정리
    • [초안] 커머스 Spring 서비스에 Clean/Hexagonal Architecture를 실용적으로 적용하기
    • [초안] 커머스 도메인 모델링: 주문·재고·노출의 세 축을 분리해서 설계하기
    • 커머스 주문 상태와 데이터 정합성 기본기
    • [초안] 쿠폰/프로모션 동시성과 정합성 기본기 — 선착순·중복 사용 방지·발급/사용/복구
    • [초안] DDD와 도메인 모델링: 시니어 백엔드 관점의 전술/전략 패턴 실전 가이드
    • [초안] Decorator & Chain of Responsibility — 행동을 체인으로 조립하는 두 가지 방식
    • 디자인 패턴
    • [초안] 분산 아키텍처 완전 정복: Java 백엔드 시니어 인터뷰 대비 실전 가이드
    • [초안] 분산 트랜잭션과 Outbox 패턴 — 왜 2PC를 피하고 어떻게 대신할 것인가
    • 분산 트랜잭션
    • [초안] e-Commerce 주문·결제 도메인 모델링: 상태머신, 멱등성, Outbox/Saga 실전 정리
    • [초안] Event Sourcing과 CQRS — 상태가 아니라 변화를 저장한다는 발상
    • [초안] F&B 쿠폰·프로모션·멤버십·포인트 설계
    • [초안] F&B · e-Commerce 디지털 채널 도메인 한 장 정리
    • [초안] F&B 주문/매장/픽업 상태머신 설계
    • [초안] F&B 이커머스 결제·환불·정산 운영 가이드
    • [초안] Hexagonal / Clean Architecture를 Spring 백엔드에 적용하기
    • [초안] 대규모 커머스 트래픽 처리 패턴 — 대규모 회원과 메가 프로모션을 버티는 설계
    • [초안] 레거시 JSP/jQuery 화면과 신규 API가 공존하는 백엔드 운영 전략
    • [초안] MSA 서비스 간 통신: Redis [Cache-Aside](../database/redis/cache-aside.md) × Kafka 이벤트 하이브리드 설계
    • [초안] Observability 입문: 시니어 백엔드가 장애를 탐지하고 대응하는 방식
    • [초안] Outbox / Inbox Pattern 심화 — 분산 메시징의 정합성 문제를 DB 트랜잭션으로 풀어내기
    • [초안] 결제 도메인 멱등성과 트랜잭션 재시도 기본기
    • [초안] 시니어 백엔드를 위한 Resilience 패턴 실전 가이드 — Timeout, Retry, Circuit Breaker, Bulkhead, Backpressure
    • [초안] REST API 버저닝과 모바일 앱 하위 호환성 — 디지털 채널 백엔드 관점
    • [초안] Spring Batch vs Event-Driven — 같은 비동기처럼 보이지만 전혀 다른 두 패러다임
    • [초안] Strategy Pattern — 분기문을 없애는 설계, 시니어 백엔드 인터뷰 핵심 패턴
    • [초안] 시니어 백엔드를 위한 시스템 설계 입문 스터디 팩
    • [초안] 템플릿 메서드 패턴 - 백엔드 처리 골격을 강제하는 가장 오래되고 가장 위험한 패턴
    • [초안] 대규모 트래픽 중 무중단 마이그레이션 — Feature Flag + Shadow Mode 실전
  • database 페이지로 이동
    • milvus 페이지로 이동
    • mysql 페이지로 이동
    • opensearch 페이지로 이동
    • qdrant 페이지로 이동
    • redis 페이지로 이동
    • vespa 페이지로 이동
    • 김영한의-실전-데이터베이스-설계 페이지로 이동
    • [초안] DB Connection Pool Saturation과 Thread Pool 격리
    • 커넥션 풀 크기는 얼마나 조정해야 할까?
    • 인덱스 - DB 성능 최적화의 핵심
    • [초안] JPA N+1과 커머스 조회 모델: 주문/메뉴/쿠폰 도메인에서 살아남기
    • [초안] MyBatis 기본기 — XML Mapper, resultMap, 동적 SQL, 운영 패턴 정리
    • [초안] MyBatis와 JPA/Hibernate 트레이드오프 — 레거시 백엔드를 다루는 시니어 관점
    • 한국어 형태소 분석기 Nori vs Lindera — OpenSearch에서 Milvus로 갈 때 어휘 검색은 유지되나
    • 벡터 DB 5종, 아키텍처는 어떻게 다른가
    • 벡터 DB 5종을 실제로 벤치마크했다 — 같은 recall에서 QPS는 얼마나 갈리나
    • 벡터 DB 어떻게 고를까 — OpenSearch · Milvus · Qdrant · Vespa · pgvector 비교
    • 벡터 DB를 실제로 도입한 사례 — 빅테크 프로덕션
    • 역정규화 (Denormalization)
    • 데이터 베이스 정규화
  • devops 페이지로 이동
    • docker 페이지로 이동
    • k8s 페이지로 이동
    • k8s-in-action 페이지로 이동
    • observability 페이지로 이동
    • [초안] 커머스/F&B 채널 장애 첫 5분과 관측성 기본기
    • [초안] 운영 데이터 정합성 장애 대응 — 결제 취소 누락과 중복 적재 런북
    • Envoy Proxy
    • [초안] F&B / e-Commerce 운영 장애 대응과 모니터링 — 백엔드 관점 정리
    • Graceful Shutdown
    • [초안] 시니어 백엔드를 위한 SLO와 Error Budget 기반 장애 대응
  • http 페이지로 이동
    • HTTP Connection Pool
    • HTTPS는 어떻게 안전한가 — TLS, 인증서, 그리고 termination
  • interview 페이지로 이동
    • [초안] AI 서비스 팀 경험 기반 시니어 백엔드 면접 질문 뱅크 — Spring Batch RAG / gRPC graceful shutdown / 전략 패턴 / 12일 AI 웹툰 MVP
    • Observability — 면접 답변 프레임
    • [초안] 시니어 Java 백엔드 면접 마스터 플레이북 — 김병태
    • [초안] NSC 슬롯팀 경험 기반 질문 은행 — 도메인 모델링·동시성·성능·AI 협업
  • java 페이지로 이동
    • concurrency 페이지로 이동
    • jdbc 페이지로 이동
    • opentelemetry 페이지로 이동
    • spring 페이지로 이동
    • spring-batch 페이지로 이동
    • testing 페이지로 이동
    • 더_자바_코드를_조작하는_다양한_방법 페이지로 이동
    • [초안] Java 동시성 락 정리 — 커머스 메뉴/프로모션 정책 캐시 갱신 관점
    • [초안] JVM 튜닝 실전: 메모리 구조부터 Virtual Threads, GC 튜닝, 프로파일링까지
    • Java의 로깅 환경
    • MDC (Mapped Diagnostic Context)
    • Java StampedLock — 읽기 폭주에도 쓰기가 밀리지 않는 락
    • Virtual Thread와 Project Loom
  • javascript 페이지로 이동
    • typescript 페이지로 이동
    • AbortController
    • Async Iterator와 제너레이터
    • CommonJS와 ECMAScript Modules
    • 제너레이터(Generator)
    • Http Client
    • Node 백엔드 운영 패턴 — Streams 백프레셔, pipe/pipeline, 멱등성 vs 분산 락
    • Node.js
    • npm vs pnpm — 어떤 기준으로 선택했나
    • `setImmediate()`
  • kafka 페이지로 이동
    • [초안] Kafka 기본 개념 — 토픽, 파티션, 오프셋, 복제
    • Kafka를 사용하여 **데이터 정합성**은 어떻게 유지해야 할까?
    • [초안] Kafka 실전 설계: 파티션 전략, 컨슈머 그룹, 전달 보장, 재시도, 순서 보장 트레이드오프
    • 메시지 전송 신뢰성
    • [초안] Spring Kafka 컨슈머 오프셋 커밋과 트랜잭션 정렬: AckMode, manual ack, 멱등 처리
  • linux 페이지로 이동
    • fsync — 리눅스 파일 동기화 시스템 콜
    • tmux — Terminal Multiplexer
  • mlops 페이지로 이동
    • llm-serving 페이지로 이동
    • Python CUDA 버전 생태계 — nvidia-smi, nvcc, pip, conda가 다 다른 버전을 말하는 이유
    • GPU 컨테이너의 CUDA 버전 호환성 — nvidia-smi부터 이미지 다이어트까지
    • Kubernetes GPU 노드에서 /run tmpfs가 꽉 차서 Pod가 안 뜰 때
    • GPU·CUDA·MPS 기초 — 자바 백엔드 개발자가 처음 만나는 그림
    • Multi-process GPU 워크로드 — 자바 ThreadPool 사용자가 만나는 모델 차이
    • ML 서비스 성능 분석 워크플로 — 자바 백엔드 트러블슈팅과 다른 점
    • 한 GPU 를 여러 프로세스가 나눠 쓰기 — Time-Slicing 과 MPS
  • network 페이지로 이동
    • Connection reset by peer는 누가 보낸 걸까 — 리버스 프록시 홉마다 TCP 연결은 따로 논다
    • L2(스위치)와 L3(라우터)의 역할 차이
    • L4와 VIP(Virtual IP Address)
    • IP Subnet
  • python 페이지로 이동
    • Python async/await — CompletableFuture·Reactor 와 다른 점, 그리고 blocking I/O 함정
    • Python 의존성 관리 — Java Maven/Gradle 사용자가 만나는 첫 충격
    • FastAPI 기초 — Spring Boot 사용자가 빠르게 익히는 법
    • Java 개발자를 위한 Python 심화 — OOP·데코레이터·컨텍스트 매니저
    • PyTorch 기초 — 텐서, 디바이스, 그리고 모델 로딩이 무거운 이유
    • Java 개발자를 위한 Python 문법 핵심
    • ThreadLocal 에서 contextvars 로 — Python 의 요청 컨텍스트 전파
    • OCR 동작 원리 — Layout · Text · Post-process 3단계
    • Python 서버의 RSS 가 안 줄어드는 이유 — gc.collect 의 한계와 malloc_trim
  • rabbitmq 페이지로 이동
    • [초안] RabbitMQ Basics — 실전 백엔드 관점에서 정리하는 메시지 브로커 기본기
    • [초안] RabbitMQ vs Kafka — 백엔드 메시징 선택 기준과 실전 운영 관점
  • security 페이지로 이동
    • [초안] 시니어 백엔드를 위한 보안 / 인증 스터디 팩 — Spring Security, JWT, OAuth2, OWASP Top 10
    • [초안] Spring Security 6.x OAuth2 + JWT 상용 인증 설계 — Grant 선택, Resource Server, Refresh Rotation, 로그아웃
  • task 페이지로 이동
    • ai-service-team 페이지로 이동
    • nsc-slot 페이지로 이동
    • sb-dev-team 페이지로 이동
    • the-future-company 페이지로 이동
  • testing 페이지로 이동
    • [초안] 시니어 Java 백엔드를 위한 테스트 전략 완전 정리 — 피라미드부터 TestContainers, 마이크로벤치, Contract까지
  • travel 페이지로 이동
    • 오사카 3박 4일 일정표: 우메다 쇼핑, USJ, 난바·도톤보리, 오사카성
  • web 페이지로 이동
    • [초안] HTTP / Cookie / Session / Token 인증 기본기 — 레거시 JSP와 모바일 API가 공존하는 백엔드 관점
FOS-BLOG · FOOTERall systems normal·v0.1 · 2026.04.27·seoul, kr
Ffos-blog/study

개발 학습 기록을 정리하는 블로그입니다. 공부하면서 기록하고, 기록하면서 다시 배웁니다.

visitors
01site
  • Home↗
  • Posts↗
  • Categories↗
  • About↗
02policy
  • 소개/about
  • 개인정보처리방침/privacy
  • 연락처/contact
03categories
  • AI↗
  • Algorithm↗
  • DB↗
  • DevOps↗
  • Java/Spring↗
  • JS/TS↗
  • React↗
  • Next.js↗
  • System↗
04connect
  • GitHub@jon890↗
  • Source repositoryjon890/fos-study↗
  • RSS feed/rss.xml↗
  • Newsletter매주 1 회 · 한 편의 글→
© 2026 FOS Study. All posts MIT-licensed.
built with·Next.js·Tailwind v4·Geist·Pretendard·oklch
fos-blog/task/문서 파싱 서비스 성능 최적화 — OCR 병…
system

문서 파싱 서비스 성능 최적화 — OCR 병렬화부터 GPU 직렬 추론 실측까지

진행 기간: 2026.05 2026.07 문서 파싱 서비스(Playground 문서 파싱 파이프라인 — 기여 개요에서 전체 맥락을 다뤘다)의 처리 속도를 여섯 차례에 걸쳐 손봤다. OCR 호출 구조를 병렬로 바꾸고, 안 쓰는 렌더링을 지우고, PPTX 이미지 처리 방식을 통째로 갈아엎었다. 그리고 마지막 최적화에서는 로컬 벤치에서 30% 빨라진 것을 운영...

2026.07.13·7 min read·0 views·SERIES · AI 서비스 실전 구축·운영 · 4/8

진행 기간: 2026.05 ~ 2026.07

문서 파싱 서비스(Playground 문서 파싱 파이프라인 — 기여 개요에서 전체 맥락을 다뤘다)의 처리 속도를 여섯 차례에 걸쳐 손봤다. OCR 호출 구조를 병렬로 바꾸고, 안 쓰는 렌더링을 지우고, PPTX 이미지 처리 방식을 통째로 갈아엎었다. 그리고 마지막 최적화에서는 로컬 벤치에서 30% 빨라진 것을 운영 GPU에 그대로 올렸다가 오차 수준 개선(+1%)만 확인했다. "병렬화하면 빨라진다"는 기대가 왜 깨졌는지, 그 이유를 실측으로 파고든 과정을 남긴다.

배경 — OCR이 처리 시간의 절반을 넘게 차지한다

문서 파싱은 docling 파이프라인(레이아웃 인식 → 표 인식 → OCR → markdown 변환) 순서로 돈다. 운영 인스턴스 로그를 분석해보니 이 중 OCR 단계가 전체 처리 시간의 55.4%를 차지했다. 성능을 개선하려면 다른 어떤 구간보다 OCR 호출 구조부터 봐야 한다는 뜻이었다.

다영역 문서 OCR 순차 호출을 병렬로

문서 한 장에 인식 영역(문단, 표 셀 등)이 여러 개면, 영역마다 OCR API를 하나씩 순차로 호출하고 있었다. 영역이 10개면 API 호출도 10번, 그것도 앞 호출이 끝나야 다음이 시작되는 구조였다.

  • 영역별 호출을 ThreadPoolExecutor로 병렬 실행하도록 바꿨다
  • 매 호출마다 새 커넥션을 맺지 않도록 requests.Session을 모듈 로드 시점에 한 번만 만들어 재사용했다

호출 횟수 자체는 그대로라 OCR 과금에는 영향이 없고, 다영역 문서의 처리 시간만 줄었다. 비용을 안 건드리고 응답 성능만 개선한 셈이라 반영 판단이 쉬웠다.

페이지·영역을 하나의 풀로 통합 — idle 84%를 찾아낸 과정

영역 단위로 풀을 만들어 병렬화했지만, 곧 이상한 점을 발견했다. 같은 운영 로그를 다시 분석해보니 페이지당 OCR 영역이 평균 0.65개였다. ThreadPoolExecutor를 페이지마다 새로 만들어 영역 개수만큼 작업을 넣는 구조였는데, 풀 크기를 4로 잡아도 평균 0.65개만 채워지니 워커의 84%가 매번 비어 도는 셈이었다.

원인은 병렬화 단위를 잘못 잡은 것이었다. 영역이 아니라 페이지·영역 조합을 평탄화해서 하나의 풀에 넣어야 워커가 항상 찬다.

python
# 페이지 그룹 안의 모든 (페이지 인덱스, 페이지, 영역) 을 한 번에 평탄화
tasks = [
    (page_idx, page, region)
    for page_idx, (page, regions) in enumerate(page_jobs)
    for region in regions
]
 
with ThreadPoolExecutor(max_workers=self.options.page_concurrency) as pool:
    futures = [pool.submit(self._call_ocr_api, t) for t in tasks]
    for future in as_completed(futures):
        ...

이 김에 두 가지를 더 고쳤다.

  • PNG 인코딩 중복 제거: 기존엔 이미지 크기 검증(원본 바이트 기준)과 API 전송(PNG 인코딩) 단계가 따로 있어서, 인코딩이 두 번 일어났다. png_bytes를 한 번만 인코딩해 검증과 전송 양쪽에서 재사용하도록 정리했다
  • 실패 지표 추가: OCR 호출이 실패해도 원인(검증 실패인지, API 에러인지)을 구분할 방법이 없었다. ocr_page_failures_total{provider, reason} Counter를 붙여 실패 사유별로 집계되게 했다

이 구조 변경으로 병렬 풀이 항상 채워져 워커 활용률이 올라갔고, 인코딩 중복도 없어졌고, 실패 원인이 대시보드에서 바로 보이게 됐다.

풀 크기를 늘리는 것보다 먼저, 그 풀에 무엇을 담을지(병렬화 단위)를 잘못 잡지 않았는지부터 확인해야 한다는 걸 이 케이스에서 배웠다.

안 쓰는 이미지 렌더링 제거

docling 파이프라인은 generate_picture_images 옵션이 켜져 있으면 문서 안의 모든 이미지를 렌더링하고 base64로 인코딩해 결과 객체에 담는다. 그런데 markdown 변환기는 그 데이터를 참조하지 않았다. 렌더링도 인코딩도 결과물 어디에도 쓰이지 않는 계산이었다.

python
pipeline_options.generate_picture_images = False  # markdown 생성기가 안 쓰는 데이터라 렌더링 자체를 끔

한 줄로 끝나는 변경이었지만, 이미지가 많은 PDF에서는 렌더링·인코딩 비용이 무시할 수 없는 수준이라 약 13% 속도 개선이 있었다.

PPTX 이미지 OCR도 순차에서 병렬로

PPTX 안에 있는 이미지도 텍스트 인식이 필요하면 OCR을 거친다. 이것도 한 장씩 순차로 처리하고 있었다.

  • 슬라이드 전체의 이미지를 모아 한 번에 병렬 호출하도록 바꿨다(기본 동시 4개, 환경변수로 조절 가능)
  • 호출 전마다 인증 상태를 확인하던 요청을 없애고 연결을 재사용해 요청 수도 절반으로 줄였다
  • 단, 일본어(PaddleOCR) 경로는 그대로 직렬로 뒀다 — 병렬화 이득보다 엔진 특성상 안전한 처리가 우선이라는 판단이었다

이미지가 많은 PPTX에서 OCR 구간 시간이 줄었고, markdown 결과물이 병렬화 전후로 동일한지 회귀 테스트로 확인했다.

PPTX 이미지 처리 구조를 통째로 바꾼 사고

병렬화보다 먼저 손봐야 했던 문제가 있었다. PPTX를 변환할 때 이미지를 일단 본문에 문자열(base64)로 넣었다가, 나중에 그 문자열을 다시 꺼내 처리하는 왕복 구조였다.

이미지 1,498장이 든 문서를 변환하다가 문제가 터졌다. 본문이 604배로 부풀고 변환 시간이 45배로 늘었다. base64 문자열이 문서 트리 곳곳에 흩어져 있으니 순회할 때마다 그 큰 문자열을 복사하고 파싱하는 비용이 곱절로 쌓인 것이다. 게다가 이미지 중 WMF 포맷 하나가 디코드에 실패하면 그 시점에서 전체 이미지 추출이 통째로 멈추는 구조라, 이미지가 많은 문서일수록 실패 확률도 같이 올라갔다.

해결은 왕복을 없애는 것이었다.

  • 이미지를 본문 문자열에 넣지 않고, PPTX 파일에서 직접 추출한다
  • 추출한 이미지를 슬라이드에 등장하는 순서로 본문과 매칭한다
  • 이미지 하나가 추출에 실패하면(WMF 디코드 실패 등) 그 이미지만 건너뛰고 나머지는 계속 처리한다 — 실패를 격리해 전체를 막지 않는다
python
try:
    img.save(buf, format="PNG")
except ValueError:
    # PIL이 못 읽는 포맷(WMF 등) — SVG 추출 재시도, 그마저 실패하면
    # 자리는 남기고 이미지만 비움 (전체 순회는 계속)
    images.append((None, placeholder))

전후 변환 결과물이 바이트 단위로 동일한지 확인했고, 순서 매칭 로직은 단위 테스트 9건으로 커버했다. 대용량 PPTX에서 본문이 부풀고 느려지는 구조적 원인을 없앤 변경이라, 이번 최적화 중 실제 장애 방지 효과가 가장 큰 항목이었다.

docling threaded pipeline — 기대와 실측이 갈린 지점

여기까지는 전부 "고쳤더니 빨라졌다"였다. 마지막 항목은 다르다.

docling에는 페이지를 나눠 병렬로 처리하는 threaded pipeline이 있다. 이걸 켜면 당연히 빨라질 거라 생각했는데, 코드를 다시 보다가 이상한 걸 발견했다. pipeline 설정에 pipeline_cls라는 키로 threaded 실행 클래스를 넘겨야 하는데, 실제 코드는 다른 키 이름으로 값을 넘기고 있었다. docling의 설정 객체가 모르는 키는 조용히 무시하는 구조라, 이 오타는 어떤 에러도 없이 그냥 씹혔다. 즉 이 서비스는 처음부터 threaded pipeline이 아니라 기본(단일 처리) pipeline으로 돌고 있었던 것이다.

python
# 정정 전 — 잘못된 키라 조용히 무시되고 기본 pipeline_cls(단일 처리)로 동작
PdfFormatOption(ThreadedPdfPipelineOptions=ThreadedStandardPdfPipeline, ...)
 
# 정정 후
PdfFormatOption(pipeline_cls=ThreadedStandardPdfPipeline, ...)

정정하면서 docling 관련 4개 패키지를 2.101 버전으로 함께 올렸다(메모리 해제 로직 개선분 포함). 로컬(CPU 다중 코어)에서 A/B로 재보니 30% 단축이 나왔다. 여기까지만 보면 "숨어 있던 버그를 잡아서 성능이 크게 개선됐다"는 결론으로 끝낼 수 있었다.

그런데 별도로 준비한 T4 GPU 검증 인스턴스에 같은 전후 이미지를 올려 운영과 동일한 조건으로 다시 재봤더니, 1,000쪽 처리 기준 **+1%**밖에 차이가 안 났다. 사실상 오차 범위였다.

왜 로컬과 운영에서 결과가 갈렸는가. threaded pipeline이 나누는 건 페이지 단위 작업이지, GPU 연산 자체가 아니다. 운영 환경은 GPU가 1개다. 레이아웃 인식 모델과 표 인식 모델의 추론은 그 GPU 위에서 순서대로(직렬로) 처리된다. 페이지를 스레드로 아무리 나눠도, 실제 무거운 연산인 모델 추론이 한 GPU 안에서 줄을 서서 실행되는 이상 전체 처리 시간은 거의 그대로다. 로컬 CPU 환경은 코어가 여러 개라 스레드 분할이 그대로 처리 시간 단축으로 이어졌지만, 그건 운영 환경의 병목과 다른 자원(코어 대 GPU)을 나눈 결과였다.

GPU 자원을 두고 왜 동시 추론이 안 되는지는 GPU로 LLM을 서빙한다는 것에서 다룬 것과 같은 원리다. 하나의 GPU에 여러 작업을 동시에 태워도, 실제 연산 유닛을 쓰는 시점은 결국 줄을 서게 된다.

그럼 이번 변경이 무의미했냐면 그건 아니다. 실질 가치는 병렬화가 아니라 잘못된 인자를 바로잡은 것과 docling 2.101의 메모리 해제 개선에 있었다. 1,000쪽 문서를 처리했을 때 메모리 사용량이 46% 줄었고 OOM은 0건이었다. 변환 품질 회귀도 없었다. 이 메모리 해제 개선이 실제 운영 메모리 문제에 어떻게 이어지는지는 문서 파싱 서비스, 리소스가 새는 자리를 하나씩 틀어막은 기록에서 더 다룬다.

로컬 벤치마크의 30% 단축이라는 숫자만 보고 운영에도 같은 이득이 있을 거라 단정했다면, 성능 개선을 근거로 다른 우선순위 작업을 미루는 잘못된 판단으로 이어졌을 것이다. 검증 인스턴스에서 운영과 같은 조건으로 다시 재본 다음에야 진짜 이득이 어디 있는지 알 수 있었다.

지금 보면

이번 최적화들을 돌아보면 두 갈래로 나뉜다. OCR 호출 구조, PPTX 이미지 처리처럼 실제로 느슨했던 곳을 고친 항목은 기대한 만큼 개선이 나왔다. 반면 docling threaded pipeline은 병목이 어디 있는지 확인하지 않고 병렬화부터 기대한 경우였다. 로컬 벤치는 어떤 자원(CPU 코어 대 GPU)을 나눠 쓰는지까지 같이 봐야 운영에 대한 근거로 쓸 수 있다는 걸 이번에 실측으로 확인했다.

idle 84%도 마찬가지다. 병렬 풀을 만들어 놨다고 끝이 아니라, 그 풀에 실제로 무엇이 얼마나 채워지는지를 운영 로그로 다시 재봐야 낭비가 보인다. "병렬화했으니 빨라졌겠지"라는 가정 대신, 바꾸기 전후를 실측으로 비교하는 습관이 이번 작업들에서 가장 크게 남았다. 이 실측이 가능했던 건 문서 파싱 서비스 관측성 체계 구축 — 초기화 순서 함정과 지표 단일화에서 만든 관측 체계 덕분이었다 — 실측 없으면 개선 근거도 없다.

on this page
  • 01배경 — OCR이 처리 시간의 절반을 넘게 차지한다
  • 02다영역 문서 OCR 순차 호출을 병렬로
  • 03페이지·영역을 하나의 풀로 통합 — idle 84%를 찾아낸 과정
  • 04안 쓰는 이미지 렌더링 제거
  • 05PPTX 이미지 OCR도 순차에서 병렬로
  • 06PPTX 이미지 처리 구조를 통째로 바꾼 사고
  • 07docling threaded pipeline — 기대와 실측이 갈린 지점
  • 08지금 보면
tags
📚 AI 서비스 실전 구축·운영
← PREVIOUS문서 파싱 서비스 관측성 체계 구축 — 초기화 순서 함정과 지표 단일화NEXT →문서 파싱 서비스, 리소스가 새는 자리를 하나씩 틀어막은 기록

이런 글도

  • 문서 파싱 서비스 — 코드 구조 정리와 파싱 품질 회귀 검증
    진행 기간: 2026.05 \ 2026.07 모놀리식 파서 서비스 하나를 두 달 동안 도메인별로 쪼개고, 그 과정에서 파싱 결과가 안 깨졌는지 확인할 방법을 처음부터 다시 설계한 기록이다. 문서 파싱 서비스(Playground의 OCR 기반 문서→markdown 변환 API)는 PDF·이미지·오피스 문서를 마크다운으로 변환하는 FastAPI 서비스다. 이...
    📁 system
    system
    2026.07.13
  • 문서 파싱 서비스 관측성 체계 구축 — 초기화 순서 함정과 지표 단일화
    진행 기간: 2026.05 2026.07 문서 파싱 서비스(Playground의 OCR 기반 문서→markdown 변환 API)를 운영하면서 관측성(observability) 체계를 처음부터 만들었다. 다음 다섯 가지를 다뤘다. - /metrics 엔드포인트 신규 도입 - 운영 배포 직후 터진 메트릭 누락 사고 대응 - 무중단 컨테이너 교체를 위한 drai...
    📁 system
    system
    2026.07.13
  • 문서 파싱 서비스, 리소스가 새는 자리를 하나씩 틀어막은 기록
    진행 기간: 2026.05 2026.07 > 기여 전체 개요는 Playground 문서 파싱 파이프라인 — 기여 개요 참고. 본 글은 그중 메모리·프로세스·이미지 크기 안정화만 모은 기록이다. 문서 파싱 서비스를 운영하면서 겪은 리소스 문제 다섯 건을 정리한다. 다룬 문제는 다음과 같다. - 워커 강제 종료 - 대용량 엑셀 메모리 - 컨테이너 좀비 프로세스...
    📁 system
    system
    2026.07.13
  • Playground 문서 파싱 파이프라인 — 기여 개요
    진행 기간: 2026.05 (진행 중) AI 서비스 개발팀의 사내 LLM 워크플로 제품 Playground는 다양한 형식의 문서를 입력으로 받는다. 이 입력을 LLM이 다룰 수 있는 markdown으로 정규화하는 문서 파싱 서비스를 맡아 운영·개선했다. OCR 제품을 만드는 게 아니라, OCR을 활용해 문서를 markdown으로 변환하는 서비스다. - 스택...
    📁 system
    system
    2026.06.17

댓글 (0)