마루옵스서버 · 네트워크 · AI 기술 노트

소개문의

[Python 개발 환경 06] OCR 프로젝트 구성: 실행 파일·언어팩·API와 워커 분리

노트북·카메라·태블릿을 함께 놓은 작업 공간

참고 사진 · 노트북·카메라·태블릿을 함께 놓은 작업 공간 · 사진: Héctor Martínez oria_hector / 원본 / CC0 · 목록용 크기 조정

OCR을 만들려고 pytesseract를 설치했는데 “실행 파일을 찾을 수 없다”는 오류가 나올 수 있습니다. pytesseract는 Tesseract를 호출하는 Python 래퍼이고, OCR 엔진과 언어 데이터는 따로 준비해야 하기 때문이에요. OCR은 개발 환경의 관리 범위를 나눠 보기 좋은 예제입니다.

Python 개발 환경 · 01 선택과 venv · 02 uv · 03 conda · 04 웹 개발 · 05 데이터·AI · 06 OCR·서비스 분리

이 글의 목차

OCR에 필요한 재료와 설치 위치

재료예시설치·보관 위치
Python 코드CLI, API, 작업자프로젝트 소스
Python 패키지Pillow, pytesseractuv·venv 또는 conda의 프로젝트 환경
OCR 실행 파일Tesseract이 글은 OS 패키지로 설치
PDF 변환 도구Poppler의 pdftoppm이 글은 OS 패키지로 설치
언어 데이터eng, kor traineddataTesseract가 검색하는 tessdata 위치
입출력 파일문서·이미지·추출 텍스트권한과 보관 기간을 정한 별도 저장 공간

아래는 Tesseract 기반의 작은 CPU CLI입니다. PaddleOCR·EasyOCR 같은 다른 엔진을 쓰면 프레임워크와 모델 파일의 설치 조건도 달라집니다. 여러 엔진을 비교한다면 처음부터 한 환경에 모두 설치하기보다 엔진별 프로젝트에서 호환 조건을 확인하세요. pytesseract 설명과 Tesseract 설치 안내를 바탕으로 구성했습니다.

1. OS별로 OCR 엔진과 언어팩을 설치한다

Ubuntu 24.04의 Tesseract·언어팩

sudo apt update
sudo apt install -y tesseract-ocr tesseract-ocr-eng tesseract-ocr-kor poppler-utils fonts-dejavu-core
tesseract --version
tesseract --list-langs
pdftoppm -v

Rocky Linux 9에 엔진과 언어팩을 설치한다

sudo dnf install -y dnf-plugins-core epel-release
sudo dnf config-manager --set-enabled crb
sudo dnf install -y tesseract tesseract-langpack-eng tesseract-langpack-kor poppler-utils dejavu-sans-fonts
tesseract --version
tesseract --list-langs
pdftoppm -v

Rocky에서는 필요한 저장소와 네이티브 의존성을 확인합니다. 이 예제는 EPEL과 CRB를 사용할 수 있는 환경을 기준으로 합니다. 조직에서 별도 저장소 정책을 적용한다면 그 정책에 맞게 패키지를 준비하세요. Rocky Linux EPEL 안내도 함께 확인할 수 있습니다.

--list-langs에 eng와 kor가 있는지 확인한 뒤 진행합니다. conda-forge의 Tesseract·Poppler를 선택하는 구성도 가능하지만, 그 환경의 실행 파일 경로와 언어 데이터 포함 여부를 다시 확인해야 해요. conda 패키지를 설치했다는 이유만으로 한국어 데이터가 준비되었다고 가정하지 않습니다.

2. Python OCR 프로젝트를 만든다

ocr-demo/
├── pyproject.toml
├── uv.lock
├── .python-version
├── src/ocr_demo/
│   ├── __init__.py
│   └── cli.py
├── tools/make_test_image.py
├── tests/test_runtime.py
├── inputs/
└── outputs/
[project]
name = "ocr-demo"
version = "0.1.0"
requires-python = ">=3.12,<3.13"
dependencies = ["pillow>=10,<13", "pytesseract>=0.3.13,<0.4"]

[dependency-groups]
dev = ["pytest>=8,<10", "ruff>=0.11,<1"]

[build-system]
requires = ["hatchling>=1.27,<2"]
build-backend = "hatchling.build"

src/ocr_demo/__init__.py는 빈 파일로 만들고, cli.py에 다음 내용을 저장합니다. 입력·출력 경로와 언어를 인자로 받아 실행 위치에 맞게 조정할 수 있습니다.

import argparse
from pathlib import Path

import pytesseract
from PIL import Image


def recognize(source: Path, destination: Path, language: str) -> None:
    with Image.open(source) as image:
        image.load()
        text = pytesseract.image_to_string(image, lang=language, timeout=30)
    destination.parent.mkdir(parents=True, exist_ok=True)
    destination.write_text(text, encoding="utf-8")


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("source", type=Path)
    parser.add_argument("destination", type=Path)
    parser.add_argument("--lang", default="eng+kor")
    args = parser.parse_args()
    recognize(args.source, args.destination, args.lang)
uv python pin 3.12
uv lock
uv sync --locked
uv run --locked python -c 'import shutil; print(shutil.which("tesseract"))'

inputs/sample.png에는 직접 준비한 작고 민감하지 않은 이미지 파일을 넣습니다. 실행 파일의 검색은 프로세스의 PATH를 따르므로, 터미널과 서비스 환경에서 다를 수 있습니다. 다른 위치에 설치했다면 해당 경로를 서비스 PATH나 pytesseract 설정에 명시해야 합니다.

# inputs/sample.png를 준비한 뒤 실행
uv run --locked python -m ocr_demo.cli inputs/sample.png outputs/sample.txt --lang eng+kor
cat outputs/sample.txt

설치가 정상인지 확인할 작은 테스트 이미지

처음에는 입력 문서의 품질 문제와 설치 오류를 분리하는 편이 좋습니다. 아래 tools/make_test_image.py는 OS에서 설치한 DejaVu 글꼴로 영문 문장을 그린 테스트 입력을 만듭니다. 실제 문서의 OCR 정확도를 측정하는 벤치마크는 아닙니다.

from pathlib import Path

from PIL import Image, ImageDraw, ImageFont

font_candidates = [
    Path("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"),
    Path("/usr/share/fonts/dejavu-sans-fonts/DejaVuSans.ttf"),
]
font_path = next(path for path in font_candidates if path.exists())
image = Image.new("RGB", (900, 160), "white")
ImageDraw.Draw(image).text((20, 40), "HELLO PYTHON", fill="black",
                          font=ImageFont.truetype(str(font_path), 64))
Path("inputs").mkdir(exist_ok=True)
image.save("inputs/sample.png")
uv run --locked python tools/make_test_image.py
uv run --locked python -m ocr_demo.cli inputs/sample.png outputs/sample.txt --lang eng
cat outputs/sample.txt

tests/test_runtime.py

import shutil
import subprocess


def test_tesseract_languages():
    binary = shutil.which("tesseract")
    assert binary is not None
    result = subprocess.run([binary, "--list-langs"], check=True,
                            capture_output=True, text=True)
    languages = (result.stdout + result.stderr).splitlines()
    assert "eng" in languages
    assert "kor" in languages
uv run --locked python -m pytest

이 테스트는 실행 파일과 영문·한국어 데이터가 보이는지 확인합니다. OCR CLI에는 별도로 영문 테스트 이미지를 넣어 동작을 확인했습니다. 한국어 실문서·손글씨·표·회전된 문서의 인식률까지 검증한 테스트는 아닙니다.

3. PDF 처리는 Python 환경 바깥의 도구도 기록한다

mkdir -p outputs
pdftoppm -f 1 -singlefile -r 150 -png inputs/sample.pdf outputs/page-1
uv run --locked python -m ocr_demo.cli outputs/page-1.png outputs/page-1.txt --lang eng+kor

이 예시는 준비한 PDF의 첫 페이지를 이미지로 변환합니다. 렌더링 해상도가 높아지면 처리 시간과 메모리도 늘어날 수 있습니다. 외부 문서 업로드를 받는 실제 서비스에는 파일 크기·형식·픽셀 수·처리 시간 제한을 별도로 둡니다. CLI 예제를 웹에 그대로 붙이는 것으로 서비스 운영 조건까지 충족되지는 않습니다.

4. API와 OCR 작업자의 환경을 나눌 시점

document-service/
├── services/
│   ├── api/
│   │   ├── pyproject.toml
│   │   ├── uv.lock
│   │   └── src/document_api/
│   └── ocr-worker/
│       ├── pyproject.toml
│       ├── uv.lock
│       └── src/document_ocr/
├── contracts/
├── infra/
└── README.md

작은 개인용 프로그램은 하나의 환경으로 충분할 수 있습니다. 파일 업로드 API와 OCR 처리가 독립적으로 늘어나거나 GPU 엔진이 추가된다면, API와 작업자를 별도 환경·프로세스로 나누고 작업 ID·입력 위치·결과 형식을 계약으로 연결할 수 있어요. API 프로세스가 학습용 프레임워크 전체를 import할 필요는 없습니다.

프로젝트 구조환경 경계
스크립트 한두 개한 프로젝트·한 환경. 명세와 실행 방법을 기록
단일 웹 앱·CLI 패키지src와 tests로 나누되 런타임 환경 하나. 개발 도구는 그룹으로 분류
서로 호환되는 여러 Python 패키지uv workspace로 공통 잠금 관리 가능. 의존성 호환성이 전제
API·GPU 워커처럼 충돌하는 서비스독립 pyproject와 잠금·실행 환경. 작업 단위와 통신 계약을 분리
프런트엔드와 백엔드Node와 Python의 환경·잠금 파일을 각각 관리

모노레포는 저장소 배치 방식이고, 공통 환경을 강제하는 규칙이 아닙니다. uv workspace는 구성원들이 잠금과 가상환경을 공유하는 방향이며, 서로 충돌하는 요구사항이나 구성원별 독립 가상환경을 원할 때는 적합하지 않을 수 있습니다. uv workspace 문서도 이 제한과 독립 프로젝트·경로 의존성 대안을 설명합니다.

5. CI는 새 환경에서 복원하는 것부터 확인한다

# 새 체크아웃의 각 Python 서비스 디렉터리에서
uv sync --locked --no-default-groups --group dev
uv run --locked --no-default-groups --group dev python -m pytest
uv run --locked --no-default-groups --group dev ruff check src tests

OCR 서비스라면 CI 이미지에도 Tesseract·언어팩·Poppler를 준비해야 합니다. .venv를 복사하는 캐시보다 다운로드 캐시와 명세 기반 재생성을 우선하고, OS·아키텍처·Python·uv 버전과 잠금 파일을 캐시 조건에 포함합니다. Jenkins를 쓴다면 기존 Pipeline 글의 에이전트에 같은 OS 패키지를 준비한 뒤 각 서비스 디렉터리에서 이 명령을 실행할 수 있습니다.

환경이 꼬였을 때 확인할 순서

  1. sys.executable로 실제 Python 경로를 확인합니다.
  2. python -m pip --version, uv pip check, conda list 중 해당 도구로 설치 영역을 확인합니다.
  3. 명세와 잠금 파일을 확인합니다. 수동 설치만 하고 명세를 빠뜨린 패키지가 없는지 봅니다.
  4. 외부 실행 파일은 command -v나 shutil.which로 찾습니다.
  5. 언어 데이터·모델 파일·공유 라이브러리·드라이버 같은 환경 바깥의 조건을 확인합니다.
  6. 새 디렉터리·새 환경에서 명세로 다시 구성해 같은 문제가 재현되는지 봅니다.

이 연재는 2026년 10월 18일 공식 문서를 대조하고 Ubuntu 24.04·Rocky Linux 9 컨테이너에서 CPU 기반 예제를 확인한 기록을 바탕으로 작성했습니다. conda 환경 생성·분석 패키지 import는 별도 격리 경로에서 확인했습니다. 전체 VM 구성, systemd 운영 배포, 실제 GPU 학습·추론, 한국어 실문서의 OCR 품질은 검증 범위에 포함하지 않습니다.

연재 예제 파일 내려받기 · venv·conda 명세와 uv·웹·데이터·OCR 예제 소스, 확인한 uv 잠금 파일을 묶었습니다. 각 프로젝트 디렉터리에서 실행하며 OS 패키지는 본문의 절차로 별도 준비합니다.

댓글

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

닉네임·이메일·댓글 내용·IP 주소·브라우저 정보는 댓글 게시 및 관리에 사용됩니다. 이메일과 접속 정보는 공개 댓글에 표시하지 않습니다. 개인정보 요청은 운영자 이메일로 보내 주세요. 개인정보처리방침