안녕하세요. 누리미디어 백엔드 개발자입니다.
저는 입사 후 신규 프로젝트로 KRpia AI 베타 버전을 맡게 되었습니다.
국사편찬위원회의 방대한 XML 데이터를 파싱하고, LLM을 활용해 요약·번역·임베딩을 수행하는 이 서비스는 각 단계의 의존성 관리가 핵심입니다.
이 복잡한 여정을 관리하기 위해 제가 선택한 워크플로우 오케스트레이션 도구, Prefect 도입기를 공유합니다.
제목없음
제목없음
🔍 워크플로우 엔진 도입, 왜 필요했을까?
초기에는 단순히 파이썬 스크립트를 순차적으로 실행하는 방식을 고민했습니다.
하지만 실제 구현에 들어가자 다음과 같은 현실적인 문제들에 직면했습니다.
파편화된 배치 관리의 한계
수많은 전처리 스크립트가 각기 다른 시점에 실행되어야 하는데, 이를 크론탭(Crontab)이나 수동으로 관리하면 장애 발생 시 어느 지점에서 멈췄는지 파악하기가 매우 어렵습니다.
비동기 서비스의 의존성 해결
Gemini Batch와 같은 외부 API는 작업 요청 후 완료까지 수 시간이 걸립니다. "A 작업이 끝나야 B 작업을 시작한다"는 단순한 로직을 구현하기 위해 복잡한 상태 체크 로직을 매번 코딩해야 했습니다.
부분 실패에 대한 복구
10만 건의 데이터 중 9만 9천 건이 성공하고 1천 건이 실패했을 때, 전체를 다시 돌리지 않고 실패한 부분만 골라 재시도할 수 있는 견고한 시스템이 필요했습니다.
이러한 문제를 해결하기 위해 워크플로우 오케스트레이션 도구 도입을 결정했고, 여러 후보군 중 최종적으로 Prefect를 선택하게 되었습니다.
제목없음
제목없음
1. 왜 Airflow가 아닌 Prefect인가?
워크플로우 도구의 대세는 단연 Airflow입니다.
하지만 신규 프로젝트를 빠르게 궤도에 올려야 하는 상황에서 저희 팀은 다음과 같은 이유로 Prefect를 선택했습니다.
첫째, 압도적으로 낮은 러닝커브 (Low Learning Curve)
Airflow는 DAG를 정의하기 위해 전용 오퍼레이터(Operator)와 복잡한 설정을 학습해야 합니다. 반면 Prefect는 Pythonic 그 자체입니다. 기존에 작성한 함수 위에 @flow, @task 데코레이터만 추가하면 즉시 워크플로우로 변신합니다. 덕분에 인프라 학습 시간을 줄이고 비즈니스 로직 구현에 더 집중할 수 있었습니다.
둘째, 비동기 외부 서비스(Gemini-Batch)의 상태 관리
KRpia AI는 LLM 처리를 위해 Gemini-Batch API를 활용합니다. 이 API는 요청 후 결과가 나올 때까지 시간이 걸리는 비동기 방식입니다.단순한 크론탭(Crontab)으로는 "작업이 끝났는지" 주기적으로 체크하고 다음 스텝으로 넘겨주는 로직을 구현하기 까다롭습니다.Prefect는 이러한 비동기 외부 서비스의 상태를 폴링(Polling)하고, 완료 시점에 맞춰 후속 태스크(요약, 임베딩 등)를 트리거하는 상태 관리 스케줄러로서 최적의 대안이었습니다.
셋째, 유연한 Chunk 단위 배치 실행
방대한 XML 데이터를 한꺼번에 처리하면 메모리 부하와 타임아웃 문제가 발생합니다. 저희는 데이터를 Chunk 단위로 나누어 실행하고, 각 Chunk의 성공/실패 여부를 개별적으로 관리해야 했습니다. Prefect는 동적 파라미터 전달이 자유로워, 데이터를 쪼개어 병렬로 실행하고 상태를 추적하는 구조를 매우 쉽게 구현할 수 있었습니다.
제목없음
제목없음
누리미디어에 OCR이 필요한 이유
사실 논문의 제목, 저자, 초록(Abstract)과 같은 메타데이터만으로도 RAG와 같은 AI 서비스를 구현하는 것은 가능합니다. 하지만 DBpia가 지향하는 방향은 조금 다릅니다.
논문이란 저자가 오랜 시간 심혈을 기울여 고민하고, 실험하고, 증명해 낸 결과물입니다. 논문의 진정한 가치는 메타데이터가 아니라, 본문 전체(Full-text)에 있습니다. AI가 논문의 실제 내용 깊숙한 곳까지 검색하고 참조하여 사용자에게 답변할 수 있어야 진정한 논문 기반의 AI 서비스라고 할 수 있다고 생각합니다. 더 나아가 AI가 답변에 활용한 부분을 PDF 위에 보여준다면 AI의 환각(Hallucination)을 검증함과 동시에 답변의 신뢰도를 높일 수 있을 겁니다.
Gemini에 PDF를 업로드하고 대화를 해보면 답변에 활용한 부분의 위치를 우측 PDF 뷰어 위에 하이라이팅 해주는 것을 볼 수 있는데요.
이러한 기능을 위해선 단순히 PDF를 Markdown 형식으로 추출하는 것이 아니라, PDF에 담긴 각 내용들이 PDF 몇 페이지, 어느 좌표에 위치하는지까지 정확히 파악하고 있어야 합니다.
제목없음
제목없음
오픈소스 OCR 모델들
2025년 10월, 여러 기업이 자신들의 기술력을 자랑하듯 경쟁적으로 OCR 모델들을 오픈소스로 공개했습니다.
실제로 이때 Hugging Face에서 Trending 모델 TOP10 중 무려 5개 모델이 OCR 모델이었습니다…!
그만큼 많은 사람들이 OCR에 관심이 많다는 뜻이겠죠.
저희는 아래와 같은 기준으로 모델들을 추려 테스트를 진행했습니다.
상업적으로 이용할 수 있고
한국어를 지원하며
각 item들의 경계 상자(bounding box, bbox) 정보와 함께 json 출력을 지원하는 OCR 모델
후보 1️⃣ DeepSeek-OCR
GRPO로 학습된 딥시크로 화제가 됐던 DeepSeek AI에서 공개한 모델로, 시각적 압축(Optical Compression)이라는 개념을 도입한 VLM 기반의 모델입니다. 이미지를 적은 수의 비전 토큰으로 압축하여 처리하기 때문에 속도가 매우 빠른 편입니다. 레이아웃 분석 모듈이 별도로 존재하지 않고 VLM이 바로 json 형태로 출력을 생성하는 구조입니다.
모델 링크: DeepSeek-OCR
후보 2️⃣ PaddleOCR-VL
Baidu의 PaddlePaddle 팀이 새롭게 공개한 VLM 기반의 OCR 모델로, 다른 OCR 모델들에 비해 압도적으로 가벼운 크기임에도 OCR 벤치마크에서 최고 성능을 달성했습니다. 별도의 레이아웃 분석 모듈 PP-DocLayoutV2가 페이지 내 객체들의 이미지를 batch로 전송하면 VLM이 각 이미지에 대해 OCR을 수행하는 구조입니다.
모델 링크: PaddleOCR-VL
후보 3️⃣ Dolphin1.5 + olmOCR-2-7B-1025-FP8 파이프라인
olmOCR-2-7B-1025-FP8은 AI2라는 기업에서 개발한 OCR 모델로, 개인적으로 체감하기에 가장 성능이 좋다고 느꼈던 모델입니다. 하지만 아쉽게도 Markdown 출력만을 지원했습니다. 이에 bytedance에서 개발한 Dolphin 1.5 모델로 레이아웃 분석만을 수행하고, olmOCR-2이 페이지 내 객체들을 디코딩하여 OCR을 수행하도록 하는 파이프라인을 구현했습니다.
모델 링크: Dolphin1.5
모델 링크: olmOCR-2-7B-1025-FP8
테스트 결과
DeepSeek-OCR은 타 모델 대비 글자 인식 정확도가 떨어졌으며, 한글을 한자로 출력하거나 띄어쓰기가 자연스럽지 못한 문제를 발견했습니다. PaddleOCR-VL은 모델 크기에 비해 성능이 매우 준수했고, Dolphin1.5 + olmOCR-2-7B-1025-FP8 역시 매우 정확한 OCR 성능을 보였습니다.
테스트 결과를 정리해 보면 다음과 같습니다.
테스트 결과 (*속도는 H100 1장 기준)
제목없음
제목없음
PaddleOCR-VL을 선택한 이유
아무래도 더 큰 모델이 좋은 성능을 보이겠지만, 약 500만 건에 달하는 논문에 대해 OCR을 수행해야 했기에 처리 속도를 고려해야 했습니다. 그래서 약 1B 사이즈로, 속도 대비 성능이 가장 우수한 PaddleOCR-VL로 최종 모델을 선정했습니다.
DBpia에서 보유한 PDF 파일의 페이지 수 기준, 3개월 정도면 전체 논문 OCR을 완료할 수 있는 일정이 나왔습니다.
(11월 7일 이후로 PaddleOCR-VL은 flash-attn과 vLLM 서빙을 모두 지원합니다.)
PaddleOCR-VL의 개행 문자 생성 문제
그런데 한 가지 문제를 발견했습니다.
위와 같이 일부 문단에서 한 문단을 자연스럽게 연결하지 못하고 각 line을 생성한 뒤, 매 line마다 줄 바꿈을 해버리는 현상을 발견했습니다. 각 줄의 간격이 멀다고 판단했는지 개행 문자 토큰을 생성한 다음, 그다음 line의 첫 토큰을 생성하는 이슈가 있었습니다.
PaddleOCR-VL은 한 문단에 대한 전체 이미지 조각을 입력받기 때문에 한 문단 내에서 개행 문자를 생성할 일은 없습니다.
따라서 객체의 label이 한 문단(text)인 경우, 그 문단 내에서 의도적으로 개행 문자 토큰 생성을 억제한다면, 자연스럽게 그다음으로 확률이 높은 토큰을 출력하도록 유도할 수 있습니다.
tokenizer.json · PaddlePaddle/PaddleOCR-VL at main 를 열어서 개행 문자 토큰의 id를 확인해 봅시다.
아스키(ASCII) 코드상에서 줄 바꿈 제어 문자인 의 토큰 id가 23번이네요.
그럼, 이제 VLM이 매 토큰을 생성할 때 23번 토큰을 생성하지 못하도록 억제해 봅시다.
이를 위해 vllm의 Logits Processors라는 기능을 활용할 겁니다. PaddleOCR-VL의 파이프라인에서는 vllm으로 VLM을 서빙하여 OCR을 수행하는데, Logits Processors 기능을 활용하면 VLM의 logit 값을 조정할 수 있습니다.
OpenAI API 형태를 통해 요청을 보낼 때 logit_bias 파라미터로 {토큰ID: 바이어스값} 와 같이 딕셔너리 형태로 값을 전달해 주면 됩니다.
kwargs["logit_bias"] = {23: -100}와 같이 전달하게 되면 23번 토큰의 logit 값에 -100이라는 값이 더해지게 되어 개행 문자 토큰의 확률은 거의 0으로 수렴합니다.
그럼, 아래 그림과 같이 그다음으로 확률이 높은 토큰이 채택되면서 한 문단이 자연스럽게 연결되게 됩니다.
출처: Gemini의 ‘동적뷰’ 기능으로 구현한 페이지 (토큰 확률 실제값 아님)
실제로 개행 토큰 억제를 적용하고 나니 한 문단을 매끄럽게 생성해 내는 것을 확인했습니다.
원본 PaddleOCR-VL 결과
개행 문자 토큰 생성 억제 수정 후 결과
제목없음
제목없음
GPU는 열일 중...
이제 남은 건 약 500만 건의 논문을 처리하는 일뿐입니다.
지금 누리미디어의 서버실에서는 GPU가 열심히 열과 소음을 내며 OCR을 수행 중입니다.
OCR을 시작으로, 본문 전체를 읽고 이해하는 진정한 원문 기반 AI 서비스로 도약하려 합니다.
앞으로 한층 더 업그레이드될 AI 서비스들을 기대해 주세요!!