이미지 한 장에서 여러 정보를 추출하는 API: OCR·객체 검출·QR 처리의 경계

방문증 한 장에는 이름과 소속, 얼굴 사진, 출입용 QR 코드가 함께 들어 있습니다. 이미지는 하나지만 필요한 답은 하나가 아닙니다.
이 글은 OCR, 객체 검출, QR 판독 기능을 소개하는 목록이 아닙니다. 같은 이미지를 세 처리 경로가 읽을 때 입력 자격을 어디서 판정하고, 한 작업이 실패해도 다른 결과를 어떻게 보존하며, 검출한 사실을 업무 결정과 어떻게 분리할지를 설명합니다.
예제는 bluetape4k-image의 Spring Boot 이미지 인텔리전스 API를
사용합니다. 방문증을 대표 시나리오로 삼지만, 목표는 방문증 전용 종합 서비스를 만드는 것이 아닙니다.
입력 자격 판정·병렬 처리·부분 실패·정책 분리의 재사용 가능한 기본 구조를 코드로 확인하는 데 초점을 맞춥니다.
한 이미지에 정보가 하나만 있는 것은 아니다
섹션 제목: “한 이미지에 정보가 하나만 있는 것은 아니다”이미지 처리 API를 기능 이름 하나로 시작하면 응답도 그 기능에 맞춰 좁아집니다. OCR API는 문자열을, 바코드 API는 코드 값을, 객체 검출 API는 분류명과 신뢰도를 반환합니다. 그런데 실제 업무의 입력은 기능별로 깔끔하게 나뉘지 않습니다.
| 사례 | OCR이 읽는 정보 | 객체 검출이 찾는 영역 | 바코드·QR이 읽는 값 | 정책 예시 |
|---|---|---|---|---|
| 방문증·출입증 | 이름, 소속, 방문 목적 | 얼굴이나 민감 영역 | 출입 식별자 | 허용, 수동 검토, 공개 미리보기 제한 |
| 배송 라벨 | 수취인, 주소, 품목 | 취급 표식이나 문서 영역 | 운송장 번호 | 주소 마스킹, 재촬영 요청 |
| 상품 라벨 | 상품명, 성분, 유통기한 | 로고나 상품 영역 | 상품 코드 | 필수 표시 검토, 등록 보류 |
세 사례는 모두 “하나의 이미지에 여러 정보가 있을 경우 어떻게 추출할 것인가?”라는 질문으로 모입니다. 한 기능이 다른 기능의 부속 단계가 되는 구조보다, 같은 자격 판정을 통과한 이미지를 서로 다른 분석기가 읽고 결과를 나중에 조합하는 구조가 자연스럽습니다.
이 구조에서도 OCR 정확도, 검출 모델 선택, QR 오류 복원 능력은 각각 별도의 문제입니다. Part 1에서는 알고리즘 내부보다 세 기능을 한 API에 연결할 때 필요한 경계를 먼저 살펴봅니다.
방문증으로 처리 경계를 고정한다
섹션 제목: “방문증으로 처리 경계를 고정한다”대표 시나리오는 행사장 방문증 접수입니다. 사용자가 방문증 이미지를 올리면 서비스는 다음 사실을 얻으려 합니다.
- OCR로 이름과 소속처럼 읽을 수 있는 텍스트가 있는지 확인합니다.
- 객체 검출로 얼굴 영역이 하나인지, 민감 영역이 포함됐는지 확인합니다.
- QR 판독으로
visitor:로 시작하는 출입 식별자가 하나인지 확인합니다. - 세 결과를 방문증 정책에 전달해
ALLOW,MANUAL_REVIEW,REJECT,QUARANTINE중 하나를 선택합니다.
여기서 1~3은 이미지에서 얻은 분석 사실이고, 4는 서비스가 내리는 업무 결정입니다. 예를 들어 QR 판독기가
visitor:PASS-001을 읽었다는 사실만으로 출입을 허용할 수는 없습니다. 얼굴 수, OCR 결과, 민감 영역,
처리 실패 여부를 함께 보고 정책이 결정해야 합니다.

처리 경로는 입력만 같고 답은 다르다
섹션 제목: “처리 경로는 입력만 같고 답은 다르다”통합 API라고 해서 세 결과를 억지로 하나의 공통 결과형에 넣을 필요는 없습니다. 공통인 것은 자격 판정을 통과한 입력 이미지와 요청 수명입니다. 결과 필드와 “아무것도 찾지 못했다”는 의미는 처리 경로마다 다릅니다.
| 처리 경로 | 현재 API의 대표 결과 | 빈 결과의 의미 | 사용할 수 없음 | 처리 실패 |
|---|---|---|---|---|
| OCR | text, pageCount | 읽을 텍스트가 없음 | OCR 공급자 미설정 | 제한 시간 또는 공급자 예외 |
| 객체 검출 | label, category, confidence, detector | 찾은 대상이 없음 | 검출기 미설정 | 검출기 실행 실패 |
| 바코드·QR | text, format, provider | 코드가 없음 | 판독기 미설정 | 디코더 실행 실패 |
| 정책 | action, reasons | 해당 없음 | 필요한 분석 근거 부족 | 자동 결정을 내리지 않고 검토로 전환 |
bluetape4k-image의 하위 라이브러리 결과는 좌표 영역이나 더 자세한 구조를 제공할 수 있습니다. 하지만 현재
통합 예제의 HTTP 응답은 위 표처럼 필요한 필드만 노출합니다. 라이브러리가 가진 모든 값을 API 계약에 그대로
복사하는 대신, 실제 소비자가 사용할 필드를 선택한 것입니다.

먼저 입력 자격을 판정하고 한 번만 디코딩한다
섹션 제목: “먼저 입력 자격을 판정하고 한 번만 디코딩한다”세 분석기를 병렬로 실행하기 전에 업로드 이미지가 분석 작업을 시작해도 되는 입력인지 먼저 판정합니다.
ImageUploadQualifier는
빈 파일, 선언된 미디어 타입, 압축 바이트 크기, 실제 파일 시그니처, 디코딩 가능 여부, 한 변의 길이와 전체
픽셀 수를 확인합니다.
이 단계가 판정하지 않는 것도 분명히 해야 합니다. OCR이 글자를 잘 읽을 수 있는지, 얼굴이 있는지, QR 코드가 포함됐는지는 입력 자격 판정의 대상이 아닙니다. 그것까지 공통 자격 판정에 넣으면 OCR이나 QR의 업무 결과가 입력 오류로 바뀝니다.
val qualified = qualifier.qualify(upload)
val results = workflow.analyze( image = qualified.image, // 한 번 디코딩한 ImmutableImage)자격 판정을 통과한 바이트는 ImmutableImage로 한 번 디코딩됩니다. OCR, 객체 검출, QR 처리 경로는 같은
불변 이미지 객체를 읽습니다. 각 경로가 멀티파트 입력을 다시 읽고 같은 이미지를 반복해서 디코딩하지 않습니다.
업로드 바이트와 디코딩된 픽셀 크기를 함께 제한해야 하는 이유, Tesseract 같은 네이티브 OCR의 운영 조건은 OCR 서비스를 실전에서 운영하기에서 더 자세히 다룹니다. 멀티파트와 메모리 입력 경계를 API 설계 관점에서 보고 싶다면 Kotlin API 입력 경계도 함께 볼 수 있습니다.
독립 작업은 병렬로 실행하고 결과는 따로 남긴다
섹션 제목: “독립 작업은 병렬로 실행하고 결과는 따로 남긴다”입력 자격 판정이 끝나면
ImageIntelligenceWorkflow가
bluetape4k-workflow의 suspendParallelFlow로 세 작업을 실행합니다. 아래 의사코드는 실제 구현에서
핵심 실행 흐름만 남긴 것입니다.
suspendParallelFlow("image-intelligence-analysis") { execute("ocr") { context["analysis.ocr"] = runOcr(image) WorkReport.success(context) } execute("detection") { context["analysis.detection"] = runDetection(image) WorkReport.success(context) } execute("barcode") { context["analysis.barcode"] = runBarcode(image) WorkReport.success(context) }}처리 경로마다 제한 시간과 Semaphore가 따로 있습니다. OCR 공급자가 느리다고 해서 QR 공급자의 동시 실행
수까지 같은 값으로 묶을 이유가 없기 때문입니다. 이 설정은 표면적인 성능 개선을 위한 구성이 아니라
서로 다른 비용 구조를 가진 네이티브 자원과 CPU를 보호하는 실행 경계입니다.
WorkReport.Success의 의미는 특히 주의해야 합니다. 이것은 “세 분석이 모두 값을 만들었다”가 아니라
“세 워크플로 작업이 약속한 결과를 WorkContext에 기록했다”는 뜻입니다. OCR 경로가
AnalysisResult.Failed를 기록해도 작업 자체는 그 실패 결과를 정상적으로 전달했으므로
WorkReport.Success를 반환할 수 있습니다.

부분 실패를 완료 결과로 숨기지 않는다
섹션 제목: “부분 실패를 완료 결과로 숨기지 않는다”각 처리 경로는 네 가지 결과를 구분합니다.
Completed: 실제 분석 값을 만들었습니다.Empty: 공급자는 정상 실행됐지만 찾은 결과가 없습니다.Unavailable: 공급자가 설정되지 않았거나 현재 사용할 수 없습니다.Failed: 공급자가 실행됐지만 시간 제한이나 실행 오류로 결과를 만들지 못했습니다.
Empty와 Failed를 같은 빈 배열로 바꾸면 정책은 “QR이 없는 이미지”와 “QR을 확인하지 못한 이미지”를
구분할 수 없습니다. 자동 승인 여부가 달라질 수 있으므로 두 상태를 응답까지 보존해야 합니다.
ImageIntelligenceAggregator는
세 결과를 다음처럼 집계합니다.
| 집계 상태 | 처리 경로 예 | 현재 방문증 정책의 해석 |
|---|---|---|
COMPLETED | 세 경로가 Completed 또는 Empty | 필요한 방문증 정보가 모두 맞으면 ALLOW |
PARTIAL | OCR Failed, 객체 검출·QR Completed | 성공 결과는 보존하고 MANUAL_REVIEW |
FAILED | 사용할 수 있는 결과가 하나도 없음 | 실패 원인을 보존하고 MANUAL_REVIEW |
집계 상태와 업무 결정은 같은 값이 아닙니다. 세 경로가 모두 정상 실행된 COMPLETED라도 QR 값이
visitor:로 시작하지 않으면 정책은 REJECT를 선택합니다. 반대로 집계 상태가 FAILED라는 이유만으로
HTTP 500이나 임의의 거부 응답으로 바꾸지 않고, 현재 정책은 사람이 확인할 수 있도록 MANUAL_REVIEW와
구체적인 사유를 반환합니다.
외부 요청 취소는 또 다른 종류의 사건입니다.
GuardedAnalysisRunner는
자신이 설정한 제한 시간만 경로별 Failed(timeout)으로 바꾸고, 상위 코루틴의 취소는 다시 던집니다.
사용자가 요청을 취소했는데 세 경로가 모두 실패한 것처럼 포장하면 작업 수명주기와 업무 결과의 의미가 혼재되기
때문입니다.
검출한 사실과 업무 정책을 분리한다
섹션 제목: “검출한 사실과 업무 정책을 분리한다”객체 검출기가 얼굴 영역을 찾았다는 것은 분석 사실입니다. 그 얼굴을 가릴지, 원본 접근을 제한할지, 방문증 접수를 수동 검토로 보낼지는 서비스 정책입니다.
VisitorPassPolicy는
민감 영역, QR 형식과 접두사, 얼굴 수, OCR 내용, 처리 경로의 실패 상태를 조합합니다. 정책을 분석 공급자
안에 넣지 않았기 때문에 배송 라벨이나 상품 라벨로 업무를 바꿀 때 OCR·검출·QR 실행 조정 구조를 그대로 두고
결정 규칙만 교체할 수 있습니다.
이 분리는 개인정보 처리에서도 유용합니다. 검출기는 얼굴의 존재와 위치를 결과로 만들고, API 미리보기에서 얼굴을 흐리게 할지 또는 원본을 별도 권한으로 제한할지는 정책과 후처리 단계가 맡을 수 있습니다. 분석 모델을 교체하는 일과 개인정보 정책을 바꾸는 일을 같은 배포 단위로 묶지 않아도 됩니다.
다만 예제의 demo 프로필은 OCR과 객체 검출에 고정 결과를 반환하는 픽스처를 사용합니다. QR 경로만 실제
ZxingBarcodeReader를 실행합니다. 이 조합은 실행 조정 구조와 실패 계약을 재현하지만, 운영용 ML
검출기의 정확도나 처리 성능을 증명하지 않습니다. 실제 서비스는 ImageDetector 구현과 품질 측정, 모델
버전 관리, 드리프트 감시를 별도로 준비해야 합니다.
이 예제가 제공하는 것과 제공하지 않는 것
섹션 제목: “이 예제가 제공하는 것과 제공하지 않는 것”이 예제가 제공하는 것은 특정 업무의 완제품이 아니라 다음 네 경계를 조합한 실행 가능한 기준선입니다.
| 제공하는 기본 구조 | 서비스가 추가로 결정할 영역 |
|---|---|
| 업로드 자격 판정과 단일 디코딩 | 악성 파일 검사, 저장·삭제, 테넌트별 할당량 |
| OCR·검출·QR 독립 실행 | 실제 OCR·ML 공급자와 모델 품질 기준 |
Completed·Empty·Unavailable·Failed 보존 | 재시도, 회로 차단기, 프로세스 격리 |
| 분석 사실과 방문증 정책 분리 | 암호화, 마스킹, 접근 통제, 감사 이력 |
이미지 백엔드 선택도 이 구조와 별도의 결정입니다. 순수 JVM과 libvips의 처리 비용 차이는 순수 JVM에서 libvips로에서 측정값과 운영 제약을 비교합니다. 통합 API가 세 분석기를 병렬로 실행한다고 해서 디코딩과 전처리 비용이 사라지는 것은 아닙니다.
시리즈에서 이어서 볼 내용
섹션 제목: “시리즈에서 이어서 볼 내용”이번 Part 1은 전체 처리 경계를 먼저 그렸습니다. 이후 글에서는 각 경계를 실제 코드와 테스트 수준으로 좁혀서 살펴봅니다.
- Part 1: 이미지 한 장에서 여러 정보를 추출하는 API
- Part 2: 이미지 분석 전에 입력부터 판정하라
- Part 3: OCR 처리 경로를 통합 응답에 연결하기
- Part 4: 이미지 검출 결과와 처리 정책을 분리하라
- Part 5: OCR과 다른 바코드·QR 추출 계약
- Part 6: 병렬 실행과 부분 실패를 응답 계약으로 만들기
- Part 7: 방문증 이미지 처리 API 통합 예제
Part 2에서는 MIME 선언과 실제 시그니처가 다를 때, 압축 바이트는 작지만 디코딩된 픽셀이 지나치게 클 때, 이미지 헤더는 읽히지만 전체 디코딩이 실패할 때를 입력 경계에서 어떻게 구분하는지 살펴봅니다.
구현 코드와 자료 살펴보기
섹션 제목: “구현 코드와 자료 살펴보기”전체 코드는 README에서 시작해 입력, 실행 조정, 공급자, 정책 순서로 살펴보면 각 경계의 책임을 파악하기 수월합니다.
- Spring Boot 이미지 인텔리전스 API: 실행 방법, 응답 상태, 프로필과 운영 범위에서 제외한 항목을 한 번에 확인할 수 있습니다.
ImageUploadQualifier.kt: 미디어 타입, 바이트·픽셀 예산, 디코딩 경계를 구현합니다.ImageIntelligenceWorkflow.kt: 세 분석 작업을 병렬 실행하고 결과 키를 수집합니다.ImageAnalysisProviders.kt: 픽스처, Tesseract, 객체 검출, ZXing 공급자 경계를 보여 줍니다.VisitorPassPolicy.kt: 분석 결과를 방문증의 허용·검토·거부·격리 결정으로 바꿉니다.- OCR 서비스를 실전에서 운영하기: 큰 이미지 입력 제한, 네이티브 OCR과 실패 응답 계약을 자세히 설명합니다.
- 순수 JVM에서 libvips로: 이미지 전처리 백엔드를 성능과 운영 조건으로 비교합니다.
- Kotlin API 입력 경계: 멀티파트, 바이트 예산과 메모리 입력을 API 경계에서 다루는 방법을 설명합니다.
댓글
GitHub 계정으로 의견을 남기거나 reaction을 남길 수 있습니다.