feat(pdf): PDF 내보내기 기능 추가 - #205
Merged
Merged
Conversation
openhtmltopdf-pdfbox 1.1.40(순수 JVM 서버 렌더링), twelvemonkeys imageio-webp 3.13.1(webp 디코드), AWS CloudFront SDK(signed URL) 의존성 추가, 렌더 전용 스레드풀 pdfExportTaskExecutor, commitlint scope에 pdf 추가
Flyway 마이그레이션 추가, PdfExportJob/Status/Type 엔티티·enum 추가, 리포지토리 메서드로 markCompleted·markFailed CAS 전이(IN_PROGRESS일 때만)·dedup 쿼리 추가, 크리스탈 로그 사유 PDF_EXPORT_GENERATE 추가
기간 조회 레포: 답변 BETWEEN+감정 조인, 리포트 overlap+COMPLETED, 월간 V1·V2, 카운트, 보상 트랜잭션, 다운로드 인프라에 UUID 키·CloudFront signed URL·업로드 시 파일명 각인·best-effort 삭제, 발급 rate-limit, api 5가지 구현, ErrorCode 8종·409/429 예외·핸들러, CloudFront config 및 ci 추가
openhtmltopdf XHTML 조립, Pretendard 임베드·감정 레이더(Java2D)·하이라이트/divider 오버샘플 PNG·사진 cover 정사각 리샘플, 비동기 리스너로 조회→렌더→S3 업로드→confirm, 실패 시 자동 환불, 개별 사진 실패는 스킵, 완료 이벤트 발행
NotificationType.PDF_EXPORT_COMPLETED 추가, REPORT 그룹에 속하도록 하였고 딥링크 data는 jobId
백엔드 displayNameKo는 인간관계인데 실제 화면 상에서는 관계라 PDF에서만 변경
리스너 오케스트레이션 테스트(조회→렌더→업로드→confirm/환불 경로), 디자인 프리뷰 하네스(합성 사진)
반복 baked 에셋(배너·divider·그림자·아이콘·로고·섹션)을 asset: 프로토콜로 1회 서빙하도록 변경, 답변 사진도 data URI → asset:photo-N 으로 전환(base64 인라인·재디코드 없이 raw 바이트 서빙), assemble 이 XHTML+사진 바이트맵(AssembledDocument) 반환, render 가 맵을 asset: 스트림으로 서빙, 렌더 동시성 max1(단일 렌더가 공유 힙을 거의 다 써 동시 2개면 OOM)
순수 JVM 하네스로 assemble→render 실경로로 peak heap·기간·사진수·공존 스윕 측정, pdfBench 태스크(-Xmx2g 기본, -PbenchXmx/-PbenchCpu·-Dpdf.bench.*파라미터), 일반 test 는 bench 제외, 합성/실 webp 사진 소스, 시각 확인용 PDF 덤프(-Dpdf.bench.dumpPdf) 옵션
유니크 인덱스 (user_id,type,start,end) → (user_id), 진행 중 다른 조건 재요청 시 409(PDF_EXPORT_ALREADY_IN_PROGRESS, 진행 중 jobId 반환), 같은 조건은 기존대로 무과금 재사용, GET /pdf-exports/current 추가, 아카이브(GET /pdf-exports)는 COMPLETED만 노출(생성중은 /current로 분리)
fontbox 3.0.3가 라틴 런마다 GSUB 정규식을 재컴파일해서 GSUB만 삭제(GPOS·cmap·hmtx·family명 유지)
-PbenchJfr(할당 프로파일)·-PbenchGcLog(churn/live 분리) 플래그, renderMode 변형(BAOS/파일 × 메모리/scratch)으로 A·scratch peak 델타 측정, histoAtMB 고수위 live class histogram, fontDir 폰트 A/B(GSUB 유무)·assetRequests 요청횟수 카운터
byte[] render() → Path render()로 변환, 렌더 실패 시 자기 임시파일 삭제 후 rethrow, RequestBody.fromFile로 스트리밍 업로드, 업로드 후 finally에서 로컬 임시파일 정리
webp를 풀디코드 후 축소하던 것을 ImageReader 서브샘플링으로 디코드 단계서 640²로 바로 받음
다운로드를 전용 스레드 3개가 창 4장까지 앞서 받아두고 디코드는 호출 스레드가 순서대로 한 장씩 처리해 S3 대기를 디코드 뒤로 숨기도록 함
사진 1장 단계분해 프로브, 프리페치 A/B 스위치, assemble/render 분리 출력, GC 종류 고정과 GC 로그 PID 분리
렌더 대기가 20건 이상이면 start()에서 429로 거부, 실행기 큐는 40으로 늘려 상한이 항상 먼저 걸리도록 함
60분 넘게 IN_PROGRESS인 작업을 5분마다 쓸어 실패 확정 + 환불, 서버 부하 가능성 때문에 렌더는 다시 돌리지X, markFailed가 IN_PROGRESS일 때만 이기므로 두 번 쓸어도 이중 환불이 없음
정상·예외 경로는 렌더러가 스스로 정리하지만 SIGKILL·커널 OOM-kill 등은 못 잡음, 계속 쌓이면 메모리를 계속 차지하므로 부팅 시 한 번 쓸어주는 로직 추가
작업 상태와 크리스탈 로그 상태가 항상 짝이 맞는지, 잔액이 원장과 일치하는지 등 TestContainers Postgres 에서 확인
개별 사진 실패는 그 장만 스킵하는데 상한이 없어서 한 장도 성공하지 못한 경우는 job 을 실패시켜 기존 환불 경로를 태우는 로직 추가, 일부만 실패한 경우는 스킵으로 남김
유 검증 403(조회·발급), 진행 중 재요청 재사용/409, 발급 3단 가드(완료 전·만료·상한), 아카이브 COMPLETED 만, 파일명, 발급 빈도 제한
예약·완료·환불 각 트랜잭션이 실제로 커밋·롤백되는지 비트랜잭션 테스트로 확인
테스트 11건 제거(67→57), 알림 발행 실패가 완료를 실패로 기록하던 문제 수정, 실패·환불 로그 5종 추가와 장애 시 로그 폭주 억제, 어긋난 주석 정리
나답_나에게답하다_(시작일)-(종료일).pdf → 나답_(시작일)-(종료일).pdf
측정 완료로 역할이 끝나 PdfExportRenderBenchmark, PdfSyntheticData 와 pdfBench 태스크를 제거
Collaborator
|
수고하셨어요 머지 고고!!! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
📝 요약(Summary)
사용자 기록(답변 / 주간·월간 리포트)을 기간으로 골라 PDF로 내보내는 유료 기능입니다.
크리스탈 차감 → 서버 렌더링 → S3 업로드 → 7일간 다운로드.
"유료 + 비동기" 조합이라 돈과 산출물이 어긋나지 않게 하는 데 설계 대부분이 들어갔습니다.
전체 흐름
POST /pdf-exports → [TX-A] job 생성 + tryConsume 차감 + CrystalLog(PENDING) +
IN_PROGRESS
↓ 커밋 후 즉시 200 (요청 스레드는 렌더를 안 기다림)
AFTER_COMMIT + @async(pdfExportTaskExecutor)
↓ 짧은 조회로 데이터 받고 커넥션 반납 → Tx 밖에서 렌더
성공 → [TX-B] confirm 실패 → [TX-C] failAndRefund
주요 결정
트랜잭션을 3개로 쪼갬
상태 전이에 CAS
동시 1개 제한 = 부분 유니크 인덱스
렌더 = 전용 스레드풀 + 동시성 1
멈춘 작업은 60분 뒤 스윕이 환불
결과 PDF·PDFBox 문서모델을 힙 밖 임시파일로
공유 파일 변경
다른 도메인과 얽힌 부분
그 외
🔗 Related Issue
💬 공유사항
✅ PR Checklist
PR이 다음 요구 사항을 충족하는지 확인하세요.