해외송금이나 간편결제를 만들면 본인확인을 피할 수 없습니다. 규제가 요구하고, 규제가 없더라도 사고가 나면 결국 여기서 막았어야 했다는 얘기가 나옵니다.
요즘은 신분증을 찍으면 이름과 생년월일, 발급일자를 알아서 읽어냅니다. 그래서 발주 문서에 "OCR 인식률 95% 이상" 같은 숫자가 적혀 옵니다. 그 숫자만 보고 만들면 대개 다시 만듭니다.
95%가 뭐의 95%인지를 안 정했습니다
같은 "95%"가 이만큼 다른 얘기일 수 있습니다.
| 기준 | 뜻 | 사용자가 겪는 것 |
|---|---|---|
| 글자 단위 | 전체 글자 중 95%가 맞음 | 이름 세 글자 중 하나 틀림 |
| 필드 단위 | 필드 하나가 통째로 맞을 확률 95% | 5개 필드면 다 맞을 확률 77% |
| 건 단위 | 한 장에서 모든 필드가 맞을 확률 95% | 이게 진짜 체감값 |
0.95^5 = 0.77입니다. 필드별 95%짜리 엔진으로 5개를 읽으면 넷 중 한 명은 뭔가를 고쳐야 합니다.
계약서에 쓸 숫자는 건 단위여야 합니다. 그리고 그 숫자는 시험실이 아니라 실제 찍는 환경에서 재야 합니다.
인식률은 찍는 환경이 정합니다
같은 엔진인데 결과가 이렇게 갈립니다.
- 형광등 아래, 어두운 책상, 손으로 든 신분증 → 반사와 그림자
- 오래된 신분증은 인쇄가 닳아 있음
- 저가 안드로이드는 초점이 안 잡힘
- 화면 속 신분증을 다시 찍는 경우
시험실에서 95% 나온 엔진이 현장에서 70%대로 떨어지는 건 흔합니다. 엔진을 바꿔서 될 일이 아니라 찍는 단계에서 걸러야 할 일입니다.
그래서 저희는 인식보다 촬영 화면에 시간을 더 씁니다.
// 찍기 전에 막는다 — 흐리거나 반사가 심하면 셔터를 잠근다
function canShoot(frame: ImageData) {
const blur = laplacianVariance(frame); // 낮으면 초점이 안 맞음
const glare = brightPixelRatio(frame, 250); // 높으면 반사
const fill = cardAreaRatio(frame); // 가이드를 채웠는지
if (blur < 80) return { ok: false, hint: "초점이 맞지 않습니다" };
if (glare > 0.04) return { ok: false, hint: "빛 반사를 피해 주세요" };
if (fill < 0.55) return { ok: false, hint: "테두리에 맞춰 주세요" };
return { ok: true };
}
한 번 더 맞춰야 하니 귀찮긴 합니다. 그래도 다시 찍는 횟수는 줄어듭니다. 실패하고 나서 재촬영하는 것보다 찍기 전에 안내받는 쪽이 훨씬 덜 짜증납니다.
틀린 다음이 진짜 설계입니다
인식은 언제든 틀립니다. 중요한 건 틀린 다음입니다.
자동으로 통과시키면 안 되는 값이 있습니다
이름 한 글자가 잘못 읽혀도 송금은 그대로 진행됩니다. 받는 사람이 다른 사람이 됩니다.
읽어낸 값은 반드시 사용자가 눈으로 보고 고칠 수 있어야 합니다. "자동 입력"이지 "자동 확정"이 아닙니다. 자동으로 채운 칸은 직접 친 칸과 눈에 띄게 달라야 하고요.
확신이 약하면 사람에게 넘깁니다
대부분의 인식 엔진은 항목마다 확신도를 같이 돌려줍니다. 이 값으로 세 갈래를 만듭니다.
type Field = { value: string; confidence: number };
function route(fields: Record<string, Field>) {
const min = Math.min(...Object.values(fields).map((f) => f.confidence));
if (min >= 0.95) return "auto"; // 그대로 진행, 사용자 확인만
if (min >= 0.70) return "review"; // 해당 칸을 강조해 확인 요청
return "manual"; // 관리자 검토 대기열로
}
이 숫자 두 개(0.95, 0.70)가 시스템의 성격을 정합니다. 어떻게 정했는지 꼭 남겨두세요. 사람이 바뀌면 아무도 못 건드립니다.
실패한 시도도 남겨야 합니다
같은 사람이 열 번 실패했다면 기기 문제일 수도 있고 남의 신분증일 수도 있습니다. 성공한 것만 저장하면 이 신호를 놓칩니다.
남길 것: userId, 시각, 결과, 최저 확신도, 실패 사유, 기기 정보
(이미지 자체는 저장하지 않는다 — 아래 참고)
규제는 나라마다 다릅니다
일정이 가장 크게 어긋나는 지점입니다.
호주로 보내는 송금이면 그쪽 기준을 따라야 하고, 확인 항목과 보관 기간이 국내와 다릅니다. 어떤 서류를 몇 년간 어떤 형태로 보관하느냐에 따라 저장 구조 자체가 달라집니다.
- 보관이 5년이냐 7년이냐 → 파티셔닝과 만료 삭제 설계
- 원본 이미지를 보관해야 하나 → 별도 암호화 저장소
- 제3자 제공 이력을 남겨야 하나 → 접근 로그 테이블
개발 시작 전에 확정돼야 합니다. 만들고 나서 "보관이 5년이었네요"가 나오면 데이터 구조를 다시 짭니다.
개인정보는 적게 들고 있을수록 낫습니다
신분증 이미지는 새어나가면 제일 곤란한 자료입니다. 기본은 이렇습니다.
| 자료 | 어떻게 |
|---|---|
| 원본 이미지 | 인식·확인 끝나면 지운다 |
| 규제상 보관해야 하는 것 | 암호화해서 서비스 DB와 분리된 곳에 |
| 관리자 화면 | 전체 노출 금지, 필요한 항목만 마스킹 해제 |
| 기간 지난 자료 | 자동 삭제를 처음에 같이 만든다 |
마지막이 중요합니다. 만료 삭제를 나중에 하려고 미루면 결국 안 합니다. 그리고 그 자료는 계속 쌓입니다.
마스킹을 푸는 동작도 로그로 남기세요. 누가 언제 누구 걸 봤는지가 없으면 사고 났을 때 범위를 특정할 수 없습니다.
얼굴 대조를 넣을지
신분증 사진과 셀피를 맞춰보는 기능은 요청이 자주 옵니다. 기술은 어렵지 않은데 법이 무겁습니다.
얼굴은 생체정보입니다. 수집하려면 따로 동의를 받아야 하고, 뭘 어떻게 보관하는지 알려야 합니다.
대안이 있으면 대안이 낫습니다. 계좌 1원 인증이나 통신사 본인확인처럼 이미 제도로 자리 잡은 수단으로 되는 일이면 훨씬 간단하고 분쟁 소지도 없습니다. 얼굴이 아니면 안 되는 이유가 분명할 때만 가세요.
정리
eKYC에서 인식률은 여러 조건 중 하나일 뿐입니다. 순서는 이렇습니다.
- 95%가 뭐의 95%인지 정한다 (건 단위로)
- 찍는 화면에서 얼마나 거를지 설계한다
- 확신도로 자동·확인·수동 세 갈래를 나눈다
- 규제 요건을 개발 전에 확정한다
- 뭘 언제까지 들고 있을지 정하고 자동 삭제를 같이 만든다
이 다섯이 정해지면 인식률은 따라옵니다. 반대로 이걸 안 정하고 엔진부터 고르면, 몇 번을 바꿔도 같은 자리입니다.
