여권 사진은 대부분의 앱이 받게 되는 파일 중 가장 민감한 파일입니다. 얼굴, 성명, 생년월일, 여권 번호가 담겨 있고, 그것만으로 다른 곳에서 계정을 열 수 있습니다. 그런데도 많은 업로드 흐름에서 이 사진은, 그것이 애초에 존재해야 하는지 누군가 묻기도 전에 대여섯 번씩 복사됩니다.

이 글은 이 질문을 엔지니어링 관점에서 살펴봅니다. 데이터 보관에 대해 GDPR이 무엇이라고 말하는지 인용하고, 여권 이미지가 조용히 쌓이는 곳들을 하나씩 짚어 보고, 저희가 읽고, 돌려주고, 잊기라고 부르는 패턴을 설명합니다. 이미지를 읽고, 결과를 돌려주고, 사진은 아무것도 남기지 않는 방식입니다. 마지막으로 여전히 여러분의 몫으로 남는 부분을 다룹니다. 일부 사업자는 사본을 보관해야 할 의무가 있고, 2027년 7월부터는 EU가 새 법률에서 이를 명시하기 때문입니다.

이 글은 신분증을 읽고 결과를 JSON으로 돌려주는 여권 및 신분증 OCR API doc.cheap의 블로그입니다. 제품에 관한 부분은 이 점을 염두에 두고 읽어 주세요.

GDPR이 실제로 요구하는 것

GDPR은 "여권 이미지를 절대 저장하지 말라"고 하지 않습니다. 그보다 더 쓸모 있는 말을 합니다. 필요한 것을, 필요한 기간 동안만 보관하고, 그 이상은 보관하지 말라는 것입니다. Reg. (EU) 2016/679 제5조 제1항이 원칙을 정합니다. 그중 두 가지가 설계의 대부분을 결정합니다.

원칙 제5조 제1항의 문구(영어판) 이미지에 대한 의미
데이터 최소화, (c)호 "adequate, relevant and limited to what is necessary in relation to the purposes for which they are processed" 처리에 필요한 것이 성명, 생년월일, 여권 번호라면, 그 값을 읽은 뒤에는 사진 자체가 필요하지 않을 수 있습니다.
보관 기간 제한, (e)호 "kept in a form which permits identification of data subjects for no longer than is necessary for the purposes for which the personal data are processed" 모든 사본에는 종료일이 필요하며, "삭제할 틈이 없었다"는 종료일이 아닙니다.

제5조 제2항은 책임성을 더합니다. 이 원칙들을 지키고 있음을 입증할 수 있어야 합니다. 그리고 제25조 제1항, 설계에 의한 데이터 보호는 "appropriate technical and organisational measures, such as pseudonymisation, which are designed to implement data-protection principles, such as data minimisation, in an effective manner"를 요구합니다. 즉 데이터 최소화 같은 원칙을 효과적으로 지키기 위한, 가명처리 같은 적절한 기술적·조직적 조치입니다.

함께 읽으면 법률 문제가 엔지니어링 문제로 바뀝니다. 이미지 사본이 적을수록, 설명하고, 보호하고, 백업하고, 결국 비워야 하는 곳도 줄어듭니다. 한 번도 기록되지 않은 사본만이 그런 작업을 전혀 필요로 하지 않습니다.

여권 이미지가 흘러드는 곳

대부분의 팀은 의도적으로 한 곳에 이미지를 저장합니다. 문제는 아무도 고르지 않은 곳들입니다. 여러분의 흐름과 대조해 볼 목록을 정리했습니다.

위치 이미지가 그곳에 도달하는 경로
업로드 버킷 클라이언트가 먼저 오브젝트 스토리지에 업로드하고, 백엔드가 그곳에서 읽습니다. 오브젝트는 요청보다 오래 남습니다.
요청 로그 로깅 미들웨어가 요청 본문을 기록하는데, base64 이미지가 바로 요청 본문입니다.
오류 보고서 예외 추적 도구가 실패한 요청의 페이로드를 첨부합니다.
큐와 재시도 작업 메시지가 이미지를 싣고 다니고, 데드레터 큐가 실패한 메시지를 몇 주 동안 보관합니다.
백업과 스냅샷 그날 찍은 데이터베이스나 디스크 스냅샷에는 그 전에 기록된 모든 이미지가 담겨 있으며, 행이 삭제된 뒤에도 오래 남습니다.
지원 티켓 사용자가 "업로드가 안 돼서" 사진을 메일로 다시 보냅니다.
분석 도구와 세션 리플레이 도구가 선택한 파일의 미리보기까지 포함해 페이지를 기록합니다.
OCR 제공업체 문서를 읽는 서비스가 자체 보관 규칙에 따라 자체 사본을 보관합니다.

마지막 행은 여러분이 가장 통제하기 어려운 곳입니다. 자신의 로그는 고칠 수 있습니다. 공급업체가 가진 사본은 그 업체의 설정을 따르므로, 그 설정이 무엇인지 물어봐야 합니다.

패턴: 읽고, 돌려주고, 잊기

이 패턴은 간단하게 말할 수 있습니다. 이미지는 한 번의 요청 동안만, 메모리 안에만 존재합니다. 요청 밖으로 나가는 것은 읽은 결과, 즉 추출된 값입니다. 사진은 전혀 나가지 않습니다.

  1. 이미지를 곧바로 인식 단계로 보냅니다. 중간에 업로드 버킷을 두지 않습니다. 큰 파일 때문에 버킷을 피할 수 없다면, 오브젝트의 수명을 몇 분으로 두고 호출이 끝나면 삭제합니다.
  2. 응답에서 필요한 것을 즉시 사용합니다. 소지자 사진 같은 잘라낸 이미지는 그 응답 안에만 존재합니다. 셀카와 증명사진을 비교하는 흐름이라면 지금 하세요.
  3. 사진이 아니라 읽은 결과를 보관합니다. 처리에 필요한 필드를 여러분의 보관 규칙에 따라 저장합니다. 생년월일과 여권 번호도 개인정보이므로 이들에게도 종료일이 필요합니다.
  4. 이미지를 로그와 오류 보고서에서 떼어 놓습니다. 모든 호출자가 기억하리라 믿기보다, 경계의 한 곳에서 본문을 제거합니다.
  5. 결과를 기록합니다. 위 표의 각 위치에 대해 이미지가 그곳에 도달할 수 있는지, 없다면 왜 없는지를 적어 둡니다. 그 기록이 바로 제5조 제2항이 요구하는 책임성입니다.

저희 API가 이미지를 다루는 방식

doc.cheap이 같은 질문을 어떻게 다루는지, 데이터 보관 및 개인정보 페이지의 설명대로 소개합니다.

  • 이미지는 절대 저장되지 않습니다. 요청 동안 메모리에 있다가 인식 엔진에 전달되고, 응답이 작성되면 사라집니다. 어떤 디스크, 오브젝트 스토어, 로그도 이미지를 받지 않습니다.
  • 잘라낸 이미지도 저장되지 않습니다. 문서 크롭, 소지자 사진, 서명은 그것을 만든 호출의 응답으로 돌아옵니다. 나중에 GET /v1/scans/{id}로 다시 읽은 스캔은 모든 이미지 항목이 null로 설정되어 있습니다.
  • 보관할 수 있는 것은 읽은 결과뿐이며, 그것도 요청한 기간 동안만입니다. retain_hours 옵션으로 요청마다 0부터 8760시간(1년)까지 설정합니다. 명시적인 값이 언제나 계정 설정보다 우선합니다.
  • retain_hours: 0은 아무것도 기록하지 않습니다. 즉시 만료되는 행이 아니라, 행 자체가 없습니다. 정리할 것도, 백업에 남는 것도, 내보낼 것도 없습니다. 그래도 스캔은 스캔 한 건으로 집계됩니다.
  • 나머지는 계정 기본값이 처리합니다. 요청에 기간이 없으면 계정의 기록 설정이 적용됩니다. 24시간, 7일, 1개월, 1년 중 하나입니다. 새 계정은 대시보드에 기록이 보이도록 1년으로 시작합니다. 설정을 줄이면 이미 저장된 행에도 적용되며, 각 행은 자신의 생성 시각부터 계산됩니다.
  • 보관된 행에는 작은 사진 한 장이 남습니다. 긴 변 기준 최대 96 px, 최대 16 KiB의 썸네일로, 대시보드의 작업 로그에서 행을 알아볼 수 있도록 표시됩니다. API로는 읽을 수 없습니다. 썸네일은 행과 함께 사라집니다.
  • 스캔 한 건을 미리 삭제할 수 있습니다. 라이브 키로 DELETE /v1/scans/{id}를 보내면 결과, 기록 행, 썸네일이 삭제됩니다. 되돌릴 수 없습니다.

기록 보관 기간 관리 가이드에서 설정을 단계별로 다루고, 데이터 처리 방식 페이지에 요약이 있습니다.

다음은 requests를 사용한 Python 무보존 호출입니다. 공개 샌드박스 키 sk_sandbox_public은 문서에 공개되어 있고 가입이 필요 없습니다. 주소당 총 10건의 문서를 무료로 인식할 수 있고, 시간당 최대 10건의 요청을 보낼 수 있습니다. 무보존은 사용자 계정의 설정이므로 라이브 키가 필요합니다. 공개 샌드박스는 계정이 아닙니다. 모든 스캔을 작은 이미지와 함께 서비스 자체 로그에 기록하므로, 테스트 이미지만 보내고 실제 문서는 절대 보내지 마세요.

import base64
import uuid

import requests

API = "https://api.doc.cheap/v1/scans"
KEY = "sk_sandbox_public"  # 프로덕션에서는 자신의 라이브 키 사용


def read_and_forget(path):
    with open(path, "rb") as f:
        image = base64.b64encode(f.read()).decode("ascii")
    response = requests.post(
        API,
        headers={
            "Authorization": f"Bearer {KEY}",
            "Idempotency-Key": str(uuid.uuid4()),
        },
        json={
            "image": image,
            # 0: 라이브 키라면 이 스캔에 관해 API 쪽에는 아무것도 기록되지 않는다.
            # False: 이 흐름에서는 쓰지 않으므로 증명사진 크롭을 받지 않는다.
            "options": {"retain_hours": 0, "return_portrait": False},
        },
        timeout=30,
    )
    response.raise_for_status()
    scan = response.json()
    del image  # 호출이 끝나는 즉시 로컬 사본을 버린다
    if scan["meta"]["status"] != "recognized":
        return None
    # 처리에 필요한 읽은 결과를 자신의 보관 규칙에 따라 보관한다.
    return {
        "scan_id": scan["meta"]["id"],
        "document_number": scan["document"]["number"],
        "expiry_date": scan["document"]["expiry_date"],
        "birth_date": scan["holder"]["birth_date"],
        "mrz_status": scan["mrz"]["status"],
    }

무보존에는 놀라기 전에 알아 두어야 할 비용이 하나 있습니다. 보통 Idempotency-Key가 있으면 재시도 시 첫 번째 결과를 돌려받습니다. retain_hours: 0에서는 돌려줄 저장된 결과가 없으므로, 24시간 동안 같은 키로 보낸 재시도는 두 번 응답되는 대신 HTTP 409와 코드 idempotency_replay_unavailable로 거부됩니다. 이 응답을 "첫 번째 호출은 처리되었다"는 뜻으로 받아들이고, 이미 가지고 있는 결과를 사용하세요.

스캔을 다시 읽어 보면 설계의 다른 면이 보입니다. 샌드박스 키는 id가 무엇이든 아무것도 다시 읽지 못합니다. 저희는 2026년 10월 5일에 sk_sandbox_public으로 이 요청을 보냈습니다.

curl https://api.doc.cheap/v1/scans/<SCAN_ID> \
  -H "Authorization: Bearer sk_sandbox_public"

HTTP 404가 돌아왔습니다(메시지는 줄였습니다).

{
  "error": {
    "code": "not_found",
    "message": "No scan with id …",
    "docs_url": "https://doc.cheap/docs/errors/not_found"
  }
}

라이브 키도 retain_hours: 0으로 수행한 스캔이나 보관 기간이 지난 모든 스캔에 대해 같은 404를 받습니다. 이 점에서 서비스를 비교하고 있다면 여권 OCR API 비교에서 시작해 보세요. 각 서비스에 무엇을 돌려주는지만이 아니라, 이미지가 어디로 가는지를 물어보세요.

여전히 여러분의 몫인 것

읽고, 돌려주고, 잊기는 API 쪽의 사본을 없앱니다. 하지만 여러분의 사업이 무엇을 보관해야 하는지를 정해 주지는 않습니다. 일부 사업자에게 그 답은 "사본"이며, 법이 그렇게 정합니다.

EU의 새 자금세탁방지법 Reg. (EU) 2024/1624는 2027년 7월 10일부터 적용됩니다. 제90조는 이렇게 규정합니다. "It shall apply from 10 July 2027, except in relation to obliged entities referred to in Article 3, points (3)(n) and (o), to which it shall apply from 10 July 2029." 즉 2027년 7월 10일부터 적용되며, 제3조 (3)(n)과 (o)의 의무 주체에게만 2029년 7월 10일부터 적용됩니다. 기록 보존에 관한 제77조는 은행과 기타 금융회사 같은 의무 주체에게 다음을 보관하도록 합니다.

"a copy of the documents and information obtained in the performance of customer due diligence pursuant to Chapter III, including information obtained through electronic identification means;"

쉽게 말해, 고객 확인 과정에서 얻은 문서와 정보의 사본이며, 전자적 신원 확인 수단으로 얻은 정보도 포함됩니다.

제77조 제3항은 기간을 정합니다. 기록은 "retained for a period of 5 years commencing on the date of the termination of the business relationship", 즉 업무 관계가 끝난 날부터 5년 동안 보존되고, 그 후 "obliged entities shall delete personal data upon expiry of the five-year period", 즉 5년이 지나면 개인정보를 삭제해야 합니다. 제77조 제2항은 일정한 조건에서 사본 대신 "a retention of the references to such information", 즉 그 정보에 대한 참조만 보존하는 것을 허용합니다.

따라서 의무 주체라면, API 쪽의 무보존이 기록을 보관할 의무를 없애 주지는 않습니다. 기록이 놓이는 곳이 바뀔 뿐입니다. 여러분의 저장소가 유일한 사본이 되고, 앞에서 본 보관 기간 제한 원칙은 여전히 그 저장소에 적용됩니다. 업무 관계가 끝나고 5년이 지나면 삭제됩니다. 설계 작업은 위 표처럼 우연히 쌓인 더미 대신, 하나의 위치, 한 명의 책임자, 암호화, 접근 제어, 삭제 작업을 갖춘 의도적인 저장소를 만드는 것입니다.

의무 주체가 아니라면 먼저 단순한 질문을 던져 보세요. 필드를 읽고 난 뒤에도 처리 과정의 무언가가 사진을 필요로 하나요? 솔직한 답은 "아니요"인 경우가 많습니다.

체크리스트

  • "여권 이미지가 흘러드는 곳" 표의 모든 위치를 자신의 흐름과 대조해 확인했다.
  • 이미지는 곧바로 인식 단계로 가거나, 수명이 몇 분인 버킷을 거친다.
  • 잘라낸 이미지는 응답 핸들러 안에서 사용하고 어디에도 기록하지 않는다.
  • OCR 호출이 보관 기간을 의도적으로 설정하며, 다시 읽을 필요가 없으면 retain_hours: 0을 쓴다.
  • 재시도 로직이 409 idempotency_replay_unavailable 응답을 처리한다.
  • 로그와 오류 보고서는 하나의 경계에서 요청 본문을 제거한다.
  • 보관하는 필드에는 종료일이 있고, 무언가가 그것을 삭제한다.
  • 법이 사본을 요구하면, 그 사본은 자체 삭제일을 가진 하나의 의도적인 저장소에 둔다.

이 글은 엔지니어링 요약이며 법률 자문이 아닙니다. 이 글이 놓친, 이미지가 샐 수 있는 곳을 발견하면 admin@doc.cheap으로 알려 주세요.

여러분께 드리는 질문: 아무도 보관할 생각이 없었던 신분증 사본을 마지막으로 발견한 곳은 어디였나요? 아래 댓글로 알려 주세요.