인도네시아 생체인식 인증 시스템 구축: 아키텍처와 Rust 패턴
Engineering Team
인도네시아 생체인식 SIM 인증 백엔드 구축
이 글은 인도네시아 생체인식 SIM 의무화 시리즈의 3부로, 프로덕션 레벨 생체인식 인증 시스템 구축에 필요한 실용적인 아키텍처 결정과 코드 구현에 초점을 맞춥니다. 시스템 설계, 데이터 흐름, 암호화 관행, 그리고 Rust와 Axum으로 구축하는 확장 가능한 백엔드 서비스에 대해 자세히 살펴봅니다.
시스템 아키텍처 개요
프로덕션 레벨 생체인식 인증 시스템은 각각 특정 역할을 가진 여러 상호 연결된 구성 요소로 이루어집니다:
핵심 구성 요소
- 캡처 서비스(모바일/웹 SDK) — 안면 이미지 캡처와 기기 내 라이브니스 검출을 처리하는 프론트엔드 구성 요소
- API 게이트웨이 — 인증, 속도 제한, 요청 라우팅, TLS 종단의 중앙 관리
- 생체인식 처리 엔진 — 안면 이미지에서 생체인식 특징 템플릿을 추출하는 서비스
- 라이브니스 검출 서비스 — 패시브 및 액티브 라이브니스 검출 모델 실행
- IKD 통합 서비스 — 인도네시아 IKD 플랫폼과의 1:1 대조 통신 처리
- 암호화 서비스 — AES-256 키 관리, 템플릿 암호화/복호화
- 감사 로그 서비스 — 규제 컴플라이언스를 위한 모든 인증 거래 기록
- 모니터링 및 알림 — 시스템 건전성, 성능 지표, 이상 탐지
데이터 흐름 아키텍처
클라이언트 SDK → API 게이트웨이 → 생체인식 엔진 → IKD 플랫폼
↓ ↓ ↓
속도 제한 암호화 서비스 감사 로그
↓ ↓ ↓
인증 캐시 키 관리 컴플라이언스 스토리지
Rust 백엔드 구현
프로젝트 구조
생체인식 인증 서비스에 다음 Rust 프로젝트 구조를 권장합니다:
biometric-service/
├── Cargo.toml
├── src/
│ ├── main.rs # 진입점 및 서버 설정
│ ├── config.rs # 설정 관리
│ ├── routes/
│ │ ├── mod.rs
│ │ ├── verify.rs # 인증 엔드포인트
│ │ ├── health.rs # 헬스 체크
│ │ └── admin.rs # 관리 엔드포인트
│ ├── services/
│ │ ├── mod.rs
│ │ ├── biometric.rs # 생체인식 처리
│ │ ├── liveness.rs # 라이브니스 검출
│ │ ├── ikd.rs # IKD 플랫폼 클라이언트
│ │ ├── crypto.rs # 암호화 작업
│ │ └── audit.rs # 감사 로그
│ ├── models/
│ │ ├── mod.rs
│ │ ├── verification.rs # 인증 요청/응답
│ │ └── audit.rs # 감사 레코드
│ ├── middleware/
│ │ ├── mod.rs
│ │ ├── auth.rs # 인증
│ │ └── rate_limit.rs # 속도 제한
│ └── errors.rs # 오류 유형
├── migrations/
└── tests/
핵심 인증 엔드포인트
use axum::{extract::State, Json};
use chrono::Utc;
use uuid::Uuid;
/// 메인 인증 엔드포인트 — 전체 생체인식 인증 흐름 처리
pub async fn verify_biometric(
State(state): State<AppState>,
Json(req): Json<VerificationRequest>,
) -> Result<Json<VerificationResponse>, AppError> {
let transaction_id = Uuid::new_v4();
let started_at = Utc::now();
// 1. 요청 검증
req.validate()?;
// 2. 라이브니스 검출
let liveness = state.liveness_service
.detect(&req.capture_data)
.await
.map_err(|e| {
state.audit.log_failure(
transaction_id, "liveness_failed", &e
);
e
})?;
if !liveness.is_live {
return Err(AppError::LivenessCheckFailed);
}
// 3. 생체인식 템플릿 추출
let template = state.biometric_engine
.extract(&req.facial_image)
.await?;
// 4. 전송용 템플릿 암호화
let encrypted = state.crypto_service
.encrypt_template(&template)
.await?;
// 5. IKD 1:1 대조
let ikd_result = state.ikd_client
.verify(&req.nik, &encrypted)
.await?;
// 6. 감사 로그 기록
let elapsed = Utc::now() - started_at;
state.audit.log_verification(AuditRecord {
transaction_id,
nik_hash: hash_nik(&req.nik),
liveness_score: liveness.confidence,
match_score: ikd_result.score,
verified: ikd_result.matched,
duration_ms: elapsed.num_milliseconds(),
timestamp: started_at,
}).await?;
Ok(Json(VerificationResponse {
transaction_id,
verified: ikd_result.matched,
confidence: ikd_result.score,
}))
}
AES-256 암호화 서비스
UU PDP와 KOMDIGI 규정의 요구 사항에 따라, 모든 생체인식 템플릿은 AES-256으로 암호화해야 합니다:
use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
use aes_gcm::aead::Aead;
use rand::RngCore;
pub struct CryptoService {
cipher: Aes256Gcm,
}
impl CryptoService {
pub fn new(key: &[u8; 32]) -> Self {
let cipher = Aes256Gcm::new_from_slice(key)
.expect("AES-256 key must be 32 bytes");
Self { cipher }
}
pub async fn encrypt_template(
&self,
template: &BiometricTemplate,
) -> Result<EncryptedTemplate, CryptoError> {
let mut nonce_bytes = [0u8; 12];
rand::thread_rng().fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);
let plaintext = bincode::serialize(template)?;
let ciphertext = self.cipher
.encrypt(nonce, plaintext.as_ref())
.map_err(|_| CryptoError::EncryptionFailed)?;
Ok(EncryptedTemplate {
ciphertext,
nonce: nonce_bytes.to_vec(),
algorithm: "AES-256-GCM".into(),
})
}
pub async fn decrypt_template(
&self,
encrypted: &EncryptedTemplate,
) -> Result<BiometricTemplate, CryptoError> {
let nonce = Nonce::from_slice(&encrypted.nonce);
let plaintext = self.cipher
.decrypt(nonce, encrypted.ciphertext.as_ref())
.map_err(|_| CryptoError::DecryptionFailed)?;
Ok(bincode::deserialize(&plaintext)?)
}
}
확장성과 성능
인도네시아 규모의 과제
2억 7,000만 명 이상의 인구와 3억 4,500만 장의 활성 SIM 카드를 보유한 인도네시아에서는 생체인식 인증 시스템의 규모 요구 사항이 막대합니다:
- 피크 부하 추정: 첫 6개월 동안 5,000만 건의 신규 등록을 가정하면, 일일 평균 약 27.8만 건의 인증
- 피크 시간대: 인도네시아의 업무 시간 패턴을 고려하면, 피크는 평균의 3-5배, 즉 일일 83-139만 건
- 초당 요청 수: 피크 시 약 16 TPS, 단 버스트 트래픽에 대한 여유 필요
수평 확장 전략
// Axum 멀티 워커 스레드 설정
#[tokio::main]
async fn main() {
let config = Config::from_env();
// 데이터베이스 연결 풀
let pool = PgPoolOptions::new()
.max_connections(config.db_max_connections) // 권장: 50-100
.min_connections(config.db_min_connections) // 권장: 10
.acquire_timeout(Duration::from_secs(3))
.connect(&config.database_url)
.await
.expect("Failed to create pool");
// 앱 구축
let app = Router::new()
.route("/api/v1/verify", post(verify_biometric))
.route("/health", get(health_check))
.layer(RateLimitLayer::new(config.rate_limit))
.layer(TimeoutLayer::new(Duration::from_secs(10)))
.with_state(AppState::new(pool, config));
// 서버 바인딩
let listener = TcpListener::bind(&config.bind_addr)
.await
.expect("Failed to bind");
axum::serve(listener, app).await.unwrap();
}
캐싱 전략
IKD 플랫폼의 부하와 응답 시간을 줄이기 위해 다단계 캐싱을 구현합니다:
- L1 캐시(프로세스 내): 최근 인증 결과의 LRU 캐시, TTL 5분
- L2 캐시(Redis): 여러 서비스 인스턴스 간에 공유하는 분산 캐시
- 주의: 규제 요구 사항으로 인해 인증 결과는 캐시 가능하지만 생체인식 템플릿은 캐시 불가
UU PDP 컴플라이언스 구현
데이터 보존 정책
/// 예약 작업: 만료된 감사 레코드 정리
pub async fn cleanup_expired_records(
pool: &PgPool,
) -> Result<u64, sqlx::Error> {
// UU PDP 요구 사항: 인증 로그 5년 보존
let cutoff = Utc::now() - chrono::Duration::days(5 * 365);
let result = sqlx::query(
"DELETE FROM audit_logs WHERE created_at < $1"
)
.bind(cutoff)
.execute(pool)
.await?;
Ok(result.rows_affected())
}
/// 사용자 데이터 삭제 요청(잊힐 권리)
pub async fn handle_deletion_request(
pool: &PgPool,
nik_hash: &str,
) -> Result<DeletionReport, AppError> {
let mut tx = pool.begin().await?;
// 관련 생체인식 템플릿 모두 삭제
let templates_deleted = sqlx::query(
"DELETE FROM biometric_templates WHERE nik_hash = $1"
)
.bind(nik_hash)
.execute(&mut *tx)
.await?
.rows_affected();
// 감사 로그 익명화(컴플라이언스 요구 사항에 따라 삭제 대신 익명화)
let logs_anonymized = sqlx::query(
"UPDATE audit_logs SET nik_hash = 'anonymized' WHERE nik_hash = $1"
)
.bind(nik_hash)
.execute(&mut *tx)
.await?
.rows_affected();
tx.commit().await?;
Ok(DeletionReport {
templates_deleted,
logs_anonymized,
completed_at: Utc::now(),
})
}
모니터링과 관측 가능성
주요 메트릭
생체인식 인증 시스템에는 포괄적인 모니터링이 필요합니다. Prometheus 메트릭과 Grafana 대시보드로 추적해야 할 주요 지표:
- 인증 성공률: 기간, 사업자, 지역별
- 라이브니스 검출 통과율: 비정상적으로 낮은 비율은 시스템 문제를 나타낼 수 있음
- IKD 응답 시간: P50, P95, P99 레이턴시
- 오류율: 오류 유형별(네트워크, 타임아웃, IKD 오류, 라이브니스 검출 실패)
- 동시 연결 수: 데이터베이스 및 IKD 플랫폼
- 큐 깊이: 비동기 처리 사용 시
use metrics::{counter, histogram};
use std::time::Instant;
pub async fn verify_with_metrics(
state: &AppState,
req: &VerificationRequest,
) -> Result<VerificationResponse, AppError> {
let start = Instant::now();
counter!("verification_requests_total").increment(1);
let result = do_verification(state, req).await;
let duration = start.elapsed().as_secs_f64();
histogram!("verification_duration_seconds").record(duration);
match &result {
Ok(resp) if resp.verified => {
counter!("verification_success_total").increment(1);
}
Ok(_) => {
counter!("verification_nomatch_total").increment(1);
}
Err(e) => {
counter!("verification_errors_total",
"error_type" => e.error_type()
).increment(1);
}
}
result
}
배포 권장 사항
인도네시아 데이터 센터
KOMDIGI 규정과 UU PDP 요구 사항에 따라, 생체인식 처리는 인도네시아 국내 데이터 센터에서 수행해야 합니다. 권장 배포 위치:
- 주요: 자카르타(대부분의 사용자와 IKD 플랫폼에 근접)
- DR: 수라바야 또는 발리(지리적 이중화)
- CDN: 전국 에지 노드(SDK 배포 및 정적 리소스용)
컨테이너화 배포
FROM rust:1.88-slim AS builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/biometric-service /usr/local/bin/
EXPOSE 3001
CMD ["biometric-service"]
결론
인도네시아 KOMDIGI 규정을 준수하는 생체인식 인증 시스템의 구축은 복잡하지만 관리 가능한 엔지니어링 과제입니다. 핵심 요점:
- 보안 우선: AES-256 암호화, TLS 1.3 전송, 제로 트러스트 아키텍처
- 컴플라이언스 중심: UU PDP 데이터 보호, 5년 감사 로그 보존, 사용자 삭제권
- 확장 가능한 설계: 수평 확장, 다단계 캐싱, 비동기 처리
- 로컬 배포: 인도네시아 국내 데이터 센터, 저대역폭 최적화, 디바이스 다양성 지원
- 포괄적 모니터링: 실시간 메트릭, 이상 탐지, 컴플라이언스 보고
Rust와 Axum으로 이 시스템을 구축하면 인도네시아의 엄격한 규제 요구 사항을 충족하면서 우수한 성능과 보안 보장을 제공할 수 있습니다.