ADR-0012: 간편인증 결과 수신을 표준창에서 간편인증-API(서버 주도)로 이전한다
- Status: Accepted
- Date: 2026-09-17
- 관련: HMH-9376(이번 작업 티켓, web) · HMH-9353(BE PR1,
lib/eziok이식) · HMH-9370(BE PR2, 테이블·허브 클라이언트·엔드포인트) · HMH-9375(BE PR3, 폴러·완료 서비스 분리) · ADR-0004/ADR-0005(App2App — 이 결정 위에서도 그대로 유지) · ADR-0008(프리필 — 이 결정으로Amended) - 배선:
packages/api/lib/auth/{schema,index}.ts,packages/feature-flags/flags.ts(siv_api_flow),apps/app/src/features/eziok/api/{machine,providers,useSivApiTransaction}.ts,apps/app/src/app/(certified-user-only)/assignment/_component/{ApiIdentityVerificationView,TelcoSelectBottomSheet,SimpleIdentityVerificationView}.tsx
Context
간편인증(eziok 표준창) 결과 수신은 사용자 브라우저(WebView 안 표준창)를 거쳐야만 우리 서버에 도달하는 구조입니다. 2026-09-10~14 프리필(ADR-0008) 전면 적용 기간(시도 n=3,333) 실측 결과 도달률은 **47.0%**였고, 이는 프리필 적용 전(n=587, 46.2%)과 사실상 같습니다 — ADR-0008이 정한 “다시 열어야 할 때” 조건(prefill_applied는 쌓이는데 전환율이 그대로일 때)을 충족했습니다. 2026-09-13 하루 실측으로는 시도 821건 중 프로세스 종료(B)+결과 유실(C) 버킷이 203건(약 25%)이었습니다.
벤더 표준창 가이드 §6을 원문으로 확인한 결과, 결과 수신 경로는 “(사용자 브라우저) keyToken → (이용기관) → (이용기관 서버) hubToken 요청” 하나뿐입니다. 서버 통지나 거래ID 조회 API는 없고, 허브는 결과를 5분만 보관합니다. 즉 표준창을 유지하는 한 앱 프로세스 종료·WebView 상태 손실로 유실된 결과는 같은 거래로는 회수 자체가 불가능합니다.
간편인증-API 제품(표준창과 별개 문서로 제공되는 가이드)은 거래 3단계(get-token → auth-enc-request → auth-result)가 전부 이용기관 서버 → 허브 호출입니다. 2026-09-16 스테이징 스파이크로 확정한 사실은 다음과 같습니다.
get-token은 기존 표준창 serviceId·키로도resultCode 2000(성공) — 별도 서비스 등록 없이 개발 환경에서 유효함을 확인했습니다(운영은 미확인).auth-result는 승인 전2800(대기), 완료2000+hubToken, 재조회1013(“이미 사용된 사용자 토큰”) — 결과는 1회성이라 폴러가 거래당 하나여야 합니다.- 인증사 앱에서 승인한 뒤 자동 복귀가 없습니다(휴대폰은 인증사 앱에 그대로 머뭅니다). 가이드에도 복귀 서술이 없습니다.
- 허브는 등록 IP만 TCP 허용하는 방화벽 뒤에 있습니다(개발은 개통 완료, 운영은 벤더 확인 대기). 호출 주체는 고정 egress IP를 가진 백엔드(Python/Celery)여야 하며, Vercel 서버리스는 해당하지 않습니다.
검토한 대안
| 대안 | 채택 안 한 이유 |
|---|---|
| 표준창 유지 + BFF가 결과를 저장 | 결과가 사용자 브라우저를 거치지 않으면 BFF에 도달할 데이터 자체가 없습니다. 회수 0건 — 표준창 가이드 §6에 서버 통지·조회 API가 없다는 사실이 이 대안을 원천적으로 막습니다 |
| 앱(native)이 결과를 폴링 | auth-result가 1회성(1013)이라 폴러는 거래당 하나여야 하는데, 폴링 주체를 앱으로 두면 앱마다 경합이 생기고 앱 생사(프로세스 종료)에 다시 의존하게 되어 지금 없애려는 문제를 그대로 남깁니다 |
| native 화면을 새로 만든다 | MWV(웹이 인증사 앱 링크를 location.href로 열어 native 셸의 기존 non-http 가로채기를 그대로 태우는 방식)로 native 코드 변경 0이 가능하므로, 화면을 native로 새로 만들 필요가 없습니다 |
Decision
간편인증 결과 수신 주체를 백엔드 하나로 옮깁니다. 서버 주도 API + MWV + 서버 저장 명의로 구성합니다.
- 서버 주도: 백엔드가
get-token/auth-enc-request로 거래를 발급하고,auth-result를 Celery 폴러가 거래별 락(Redis)으로 단일 조회합니다. 결과가 1회성이므로 폴링 주체는 이 하나뿐입니다. - MWV: 인증사 앱 링크는 웹이
window.location.href로 열어, 기존 native 셸의onShouldStartLoadWithRequestnon-http 가로채기를 그대로 이용합니다(native 코드 변경 0). - 서버 저장 명의: 인증 요청에 쓰는 이름·생년월일·휴대폰은 서버가
auth_identity_verification에서 직접 읽어 허브 공개키로 암호화합니다. 거래 시작 응답에는appLink가 담깁니다 — 웹이 인증사 앱을 열어야 하는 유일한 의도적 예외입니다. 이름·생년월일·휴대폰과 토큰은 어떤 응답에도 싣지 않으며,appLink역시 로그·RUM·Sentry에는 남기지 않습니다. - 플래그·킬스위치·자동 폴백 3층 롤백(배포 불필요): ① 웹 Datadog 플래그
siv_api_flowoff → 웹이 즉시 표준창 경로로 동작. ② 백엔드 킬스위치HMH_EZIOK_API_ENABLED=false(또는 허브 오류·5xx·네트워크 오류) → 웹이 같은 자리에서 표준창startAuth()로 자동 폴백(siv_api_fallback_to_std계측). ③vercel promote(웹)/백엔드 이전 배포로 코드 자체를 롤백.
배선은 웹 쪽만 이 티켓(HMH-9376)에서 진행하며, 백엔드 데이터·엔드포인트·폴러는 HMH-9353/9370/9375에서 이미 구현됩니다. 상세 설계는 docs/superpowers/specs/2026-09-16-siv-api-mwv-design.md, 웹 구현·문서는 간편인증 API 모드를 참고하세요.
Consequences
좋아지는 것
- 앱 생사와 무관하게 결과를 회수합니다 — 프로세스 종료(B)·결과 유실(C) 버킷의 근본 원인(결과가 브라우저만 거친다)이 사라집니다.
- 인증사 선택 UI를 우리가 소유합니다 — 표준창 내부였던 이탈 구간(A 버킷)도 우리 계측·UX 안으로 들어옵니다.
- 지원 인증사 6종을 명시적으로 통제합니다 — BE 키 파일에 있는 provider만 노출하고, 그 외는
SIV_PROVIDER_UNSUPPORTED로 인증 완료 후 실패보다 빠르게 걸러냅니다.
감수하는 것
- 인증사 앱 승인 후 자동 복귀가 없어 사용자가 수동으로 앱을 전환해야 합니다.
- 백엔드가 Celery 폴러(3초 간격·330초 상한)와 beat 스위퍼를 새로 운영해야 하고, 기본 큐를 점유합니다.
- 운영 롤아웃은 운영 허브 방화벽 개방(벤더 대기)에 의존합니다 — 개방 전에는 코드가 배포돼도 운영에서 플래그를 켤 수 없습니다.
- 지원 인증사가 BE 키 파일에 있는 6종(카카오·네이버·PASS·토스·신한·삼성패스)으로 제한됩니다.
이 결정을 다시 열어야 할 때
- 벤더가 인증사 앱 → 우리 앱 자동 복귀 스킴을 제공할 때(현재의 수동 복귀 전제가 바뀝니다).
RESULT_CONSUMED(허브 재조회1013에 대응하는 실패 사유)가 발생할 때 — 설계상 발생 불가능해야 하므로, 발생 자체가 폴러 락 설계의 결함 신호입니다.- 롤아웃 기간 API 모드 도달률이 표준창 대비 +15%p 이상 높지 않을 때(성공 기준 미달).
- 표준창 자동 폴백(
siv_api_fallback_to_std) 비율이 1%를 초과할 때 — API 경로 자체의 안정성 문제로 판단하고 플래그를 내립니다.