PDF 렌더러 교체 이력
2026년 2월 8일부터 2026년 6월 21일 사이에 report-svc는 현재 PDF 경로에 정착하기 전까지 렌더링 스택을 여러 번 바꿨습니다. 이 변화는 렌더러 관련 커밋에서 가장 뚜렷하게 보입니다.
| 날짜 | 커밋 | 변경 |
|---|---|---|
| 2026-02-08 | 543ebb3b | Playwright가 WeasyPrint로 대체되었습니다. |
| 2026-03-15 | 373b6d44, cd6ea3b7, 4ac41e5b, bdd2fbfd, 5311a873 | WeasyPrint는 Playwright로 교체된 후 브라우저 워밍업 및 병렬 백엔드 가져오기로 최적화되었습니다. |
| 2026-03-15 | ae1fc2c2, 6ca3ccfc, 5822041b | WeasyPrint+PyMuPDF는 imgkit로 교체되었고, wkhtmltopdf용 Bookworm에 고정된 후 같은 날 되돌렸습니다. |
| 2026-06-21 | 6ee24b1d, bc2595a8, 21059827 | 렌더링이 ReportLab으로 이동하고 일괄 작업이 PDF를 MinIO에 직접 업로드하기 시작했으며 이미지 빌드가 복구되었습니다. |
거듭해서 변한 것
불안정한 경계는 보고서 데이터 수집이 아니었습니다. 렌더링 런타임이었습니다.
- Playwright는 2026년 3월 15일 시리즈 동안
services/report/main.py및services/report/services/report_generator.py에서 브라우저 프로세스와 시작 워밍업을 요구했습니다. - WeasyPrint 및
fitz는 렌더링을 다시 Python으로 옮겼지만 여전히services/report/services/report_generator.py의 HTML 템플릿과 PDF-이미지 변환에 의존했습니다. imgkit는 이를wkhtmltoimage로 대체했습니다. 이는services/report/Dockerfile에 OS 패키지 종속성을 추가하고6ca3ccfc의 Bookworm에 고정해야 했습니다.- ReportLab은
services/report/src/adapters/reportlab_renderer.py에서 직접 PDF를 그려 런타임 경로에서 브라우저 및 HTML 템플릿 종속성을 제거했습니다.
마지막 조치는 비동기 안전을 유지하면서 렌더링 경로를 워커 프로세스 내부에 완전히 유지합니다.
class ReportLabReportGenerator:
async def generate_pdf(self, report_data: ReportData) -> bytes:
return await asyncio.to_thread(self._generate_pdf_sync, report_data)
async def warm_up(self) -> None:
return None
출처: lumie-worker/services/report/src/adapters/reportlab_renderer.py
안정화된 설계
2026년 6월 21일에 등장한 안정적인 설계는 세 부분으로 구성됩니다.
1. PDF 바이트를 직접 렌더링
이제 GenerateReportUseCase.execute()는 {"report_bytes": bytes}를 반환하고 보고서를 스크린샷 아티팩트로 변환하지 않습니다. execute_batch()는 청크의 각 학생 항목에 대해 동일한 PDF 렌더러를 사용합니다.
2. 일괄 콜백은 저장된 파일 참조를 전달합니다
bc2595a8는 services/report/src/adapters/minio_store.py에 MinioReportStore.upload_pdf()를 도입하고 인라인 reportBytes 대신 fileKey를 보내도록 일괄 성공 콜백을 변경했습니다. 현재 스키마는 성공 페이로드에 정확히 하나의 아티팩트 참조가 포함되도록 강제합니다.
class SuccessCallbackPayload(BaseModel):
fileKey: str | None = None
reportBytes: str | None = None
@model_validator(mode="after")
def require_exactly_one_artifact_reference(self) -> SuccessCallbackPayload:
has_file_key = self.fileKey is not None
has_report_bytes = self.reportBytes is not None
if has_file_key == has_report_bytes:
raise ValueError("success callback must include exactly one of fileKey or reportBytes")
return self
출처: lumie-worker/services/report/src/schema.py
3. 이미지 빌드는 더 복잡해지지 않고 단순해졌습니다
현재 services/report/Dockerfile는 더 이상 Playwright 또는 wkhtmltopdf를 설치하지 않습니다. 빌더 단계에서 Python 종속성을 설치하고 python:3.11-slim-bookworm를 유지하며, ReportLab 글꼴 검색을 위해 글꼴 패키지와 fontconfig만 추가합니다. 21059827는 ReportLab 마이그레이션 후 fontconfig를 다시 추가한 작지만 중요한 수정입니다.
현재 정상 상태
현재 보고서 흐름은 다음과 같습니다.
services/report/src/usecase.py는 백엔드 데이터를 동시에 수집합니다.services/report/src/adapters/reportlab_renderer.py는asyncio.to_thread(...)를 사용하여 워커 스레드에서 PDF를 렌더링합니다.- 단일 보고서 경로는
services/report/main.py를 통해 인라인 PDF 바이트를 반환합니다. - 일괄 경로는
services/report/src/adapters/minio_store.py를 통해 PDF를 업로드하고services/report/src/mq/consumer.py를 통해 MinIO 키를 반환합니다.
2026년 6월 검사된 정상 상태에서 렌더링 계약, 배치 계약 및 컨테이너 이미지는 모두 동일한 방향을 가리키고 있습니다. 즉, 런타임 종속성이 적고 프로세스 간 예상치 못한 문제가 적습니다.
유지할 불변성
- 렌더러를 프로세스 내부에 유지하세요. 출력 계약에서 실제로 요구하지 않는 한 브라우저 기반 또는 외부 바이너리 기반 렌더링을 다시 도입하지 마세요.
- 비동기 서비스가 계속 응답할 수 있도록
asyncio.to_thread(...)뒤에 CPU 바인딩된 그리기 작업을 유지합니다. - 단일 메시지 성공을 위한 인라인
reportBytes와 업로드된 배치 아티팩트를 위한fileKey간에 배치 콜백 계약 분할을 유지합니다. - 이미지 변경을 최소화하세요. 렌더링 변경에 새 패키지가 필요한 경우
services/report/Dockerfile이 더 이상 가벼운 ReportLab 경로에 머물 수 없는 이유를 문서화하세요.
관련 수명주기 및 비동기 복구 패턴은 Async-First Worker Recovery를 참조하세요.