Back to Opendataloader Pdf

hancom-ai 페이지 분할 — 서브테스크 정의

docs/superpowers/plans/2026-08-21-hancom-page-chunking-tasks.md

2.5.39.1 KB
Original Source

hancom-ai 페이지 분할 — 서브테스크 정의

브랜치 feat/hancom-page-chunking 계획서 2026-08-21-hancom-page-chunking.md 방식 TDD (red → green → 리팩터). 선택 지점은 권장안 채택.

각 테스크는 완료조건(구현이 무엇을 만족해야 하는가)과 검증방법(그것을 어떻게 증명하는가 — 실행 가능한 명령/단정)을 분리해 정의한다.


T0 — 서버 한계 실측

상태: 서버 도달 불가로 차단(미완). 기본값 20으로 진행, 실측은 별건.

  • 완료조건: 한계 축(페이지 수 / 파일 크기 / 픽셀 총량) 판정.
  • 검증방법: 한계 직전·직후가 2회 재현 일치.
  • 차단 시 처리: layoutPageChunk 기본 20, 설정으로 변경 가능하게 만들어 실측 후 코드 변경 없이 조정. 계획서에 "미실측" 명기 완료.

T1 — 페이지 부분집합 PDF 추출

extractPages(byte[] pdf, List<Integer> absolutePages0Based) → byte[]

  • 완료조건
    1. 지정한 절대 0-based 페이지만 담은 새 PDF 바이트 반환, 슬라이스 순서 유지.
    2. 입력 배열 불변 (원본 훼손 금지).
    3. try-with-resources로 PDDocument 누수 없음.
    4. 범위 초과 페이지는 건너뛰고 경고 (예외로 문서 전체를 죽이지 않음).
  • 검증방법
    • mvn -pl opendataloader-pdf-core test -Dtest=HancomPdfPageSlicerTest
    • 단정: 82p 샘플에서 [30..49] 추출 → 페이지 수 20, 각 페이지 미디어박스가 원본 30..49와 일치, 원본 바이트 배열 해시 불변.
    • 경계: 빈 리스트 → 0페이지 아님(호출측이 막음, 단정으로 고정), 단일 페이지, 비연속 [5,9,40], 범위 초과 [80,999].

T2 — 상대→절대 페이지 재번호 ★급소

  • 완료조건
    1. 응답의 상대 page_number: kslice.get(k) 절대값으로 매핑.
    2. 순수 함수 — 원본 응답 트리 in-place 변형 금지 (신규 노드 반환).
    3. k < 0 || k >= slice.size() 레코드는 폐기 + WARNING.
    4. DLA 페이지 레코드 외 TSR/수식/캡션 결과의 page_number도 동일 규칙 적용.
    5. object_id는 페이지 지역 식별자이므로 변경하지 않음.
  • 검증방법
    • mvn ... -Dtest=HancomPageRenumberTest
    • 단정: 연속 [30,31,32], 비연속 [5,9,40] 매핑 정확.
    • 충돌 단정: 재번호 결과에서 절대 페이지 중복 0건 (뒤섞임 조기 검출).
    • 폐기 경로: page_number: 99 + slice 크기 3 → 결과에서 제외, 예외 없음.
    • 불변성: 입력 JsonNode를 재번호 후 재검사해 원본 page_number 유지 확인.

T3 — 분할 루프 배선

  • 완료조건
    1. convert()request.getPageNumbers()를 존중 (빈 값 → 전체 페이지).
    2. 슬라이스 크기 초과 시 DLA를 여러 번 호출, 결과를 절대 페이지로 병합·정렬.
    3. 회귀 없음: 슬라이스 크기 이하 문서는 기존과 동일한 단일 호출.
    4. TSR/수식/캡션은 원본 pdfBytes + 절대 페이지로 기존 로직 유지.
  • 검증방법
    • mvn ... -Dtest=HancomAIPageChunkingTest (MockWebServer)
    • 단정: 45페이지 요청 → DLA 호출 3회, 각 요청 FILE의 페이지 수 20/20/5, 병합 페이지 수 45, page_number 오름차순.
    • 회귀 단정: 1페이지 문서의 DLA 호출 횟수 1회, 업로드 바이트가 원본 PDF와 동일.

T4 — 부분 실패 격리

  • 완료조건
    1. 일부 슬라이스 실패 → 성공분 병합 + 실패 페이지를 failedPages로 보고.
    2. 전 슬라이스 실패 → 기존대로 IOException (문서 단위 Java 폴백).
  • 검증방법
    • 단정: 3슬라이스 중 2번째 HTTP 500 → 1·3 슬라이스 페이지는 결과에 존재, 2번째 페이지들은 getFailedPages()에 포함, 예외 없이 완주.
    • 단정: 전부 500 → IOException.

T5 — 처리기 루프와의 상호작용

  • 완료조건
    1. BACKEND_CHUNK_SIZE(50)와 클라이언트 분할(20)의 이중 분할 정리.
    2. 다중 슬라이스 문서의 evidence 원본 JSON이 전 페이지를 담는다 (lastHybridRawJson 마지막-청크-승 손실 없음).
  • 검증방법
    • 단정: 60페이지 요청 시 getLastHybridRawJson()의 DLA 페이지 수 == 60.
    • 코드 확인: hancom-ai 경로에서 처리기 청킹이 중복 분할을 만들지 않음.

T6 — 다중 페이지 검증 자산

  • 완료조건: 32p(경계)·34p(수식)·82p(스캔) 중 최소 1건이 회귀 셋에 포함.
  • 검증방법
    • 단정: 32페이지 문서 출력에 32페이지 전부 존재.
    • 내용 대조: 첫·중간·끝 페이지 텍스트가 원본 해당 페이지와 대응 (재번호 오류 검출).

T7 — 실서버 종단 검증

상태: 서버 도달 불가 → 차단. 목 서버/단위 검증으로 대체하고 별건으로 남긴다.

  • 완료조건: 82p·154p가 odl-pdfua로 태그 PDF까지 완주.
  • 검증방법: 페이지 수 일치, veraPDF 결과를 main과 대조해 신규 실패 0건.

T8 — 서브에이전트 코드리뷰 및 보안취약점 점검

  • 완료조건: 정합성(재번호), 자원(대형 PDF 메모리/누수), 동시성, 입력 신뢰 (서버 응답을 신뢰하지 않는지) 관점 리뷰 완료 및 ship-blocker 0건.
  • 검증방법: 지적 사항별 수정 또는 근거 있는 보류 기록.

선택 지점과 채택안 (일괄 진행 규칙)

선택채택안근거
이미지 분할 vs PDF 분할PDF 분할텍스트 레이어 보존 (계획서 1.4)
슬라이스 크기2030 한계에 안전 여유, 왕복 지연 억제
순차 vs 병렬 전송순차서버 동시성 미확인, 정확성 우선
부분 실패페이지 단위 격리250p를 한 슬라이스 때문에 버리지 않음
CLI 노출비노출(내부 설정)사용자 조정 대상 아님

진행 결과 (2026-08-21 완료)

테스크상태검증 결과
T0 서버 한계 실측차단실서버·스테이징 모두 도달 불가. 30은 미실측 전제. layoutPageChunk로 코드 변경 없이 조정 가능하게 처리
T1 페이지 추출완료단위 8개. 실물 3종(82p 스캔/32p 매거진/34p 수식) 텍스트 대조 0 불일치
T2 재번호 매기기완료단위 13개. 뮤테이션 검증: 매핑 무력화 시 13개 중 10개 실패
T3 분할 배선완료단위 11개. 프로브 서버로 1/20/21/30/31/82/255p 전부 PASS, 요청당 20p 초과 없음
T4 부분 실패 격리완료중간 슬라이스 500 → 나머지 페이지 보존 + failedPages 보고
T5 이중 청킹 정리완료hancom-ai는 처리기 청킹 우회. 60p 문서 증거 유실 해소. 오버플로 클램프 테스트 포함
T6 다중 페이지 자산완료32p 생성 문서 + 픽스처, 목 서버 슬라이스 replay, smoke-mock.sh 배치 검증 추가 (목 테스트 34→45)
T7 실서버 종단차단서버 불가. 목 서버로 대체 검증(32p 전 페이지 정위치, veraPDF 1/1 통과)
T8 코드리뷰·보안완료서브에이전트 2건 + CodeRabbit 3회. 결함 6건 적발(전부 조용한 페이지 손실), 각각 red 재현 후 수정. PR #693 리뷰 스레드 9건 전부 응답·resolve

실측으로 확인한 핵심

분할 전 상태: HancomAIClientgetPageNumbers()를 무시해 처리기의 50페이지 청킹이 무효였다. 100페이지 문서는 전체를 두 번 업로드하고 매번 전체 결과를 받았다.

재번호 매기기가 급소라는 증거: 재번호를 무력화한 뒤 32페이지 문서를 목 서버로 돌리면 결과가 20페이지(0..19)로 축소된다. 두 번째 슬라이스의 12페이지가 1~12페이지를 조용히 덮어쓴다. 예외도 경고도 없다.

검증 공백이 있었다: 200개 코퍼스가 전부 1페이지여서 기존 하이브리드 검증 전체가 다중 페이지 경로를 한 번도 실행하지 않았다. T6에서 32페이지 자산을 만들어 각 페이지에 자기 페이지 번호를 새겨, 오배치가 그럴듯해 보이지 않고 실패로 드러나게 했다.

리뷰가 잡은 결함 6건은 모두 같은 모양이었다: 출력이 정상처럼 보이면서 페이지가 조용히 사라진다. 예외도 경고도 없다. 이 기능이 노출된 실패 유형이 그것 하나라는 뜻이고, 페이지 번호 변환과 누락 페이지 회계가 그걸 막는 유일한 장치다. 그래서 두 지점 모두 뮤테이션으로 "테스트가 실제로 그 버그를 잡는지" 확인했다(각각 10/13, 5개 실패).

내가 쓴 테스트 2개가 정작 대상 버그를 못 잡았다: (1) TreeSet이 잘못된 페이지를 무해한 뒤쪽으로 정렬해버려 통과 — 앞쪽에 오는 page 0 케이스로 교체. (2) 클램프 테스트가 production 식을 테스트 본문에 복사해 계산 — effectiveChunkSize를 추출해 실제 호출로 변경. 초록 막대는 검증이 아니다.