MeetX(가제) 전화번호 기반 소셜 매칭
전화번호 상호 등록 매칭 + 토큰 경제 기반 소셜 매칭 서비스
배경
'잃어버린 인연을 다시 찾아주는' 소셜 매칭 플랫폼이다. 사용자가 그리운 사람의 전화번호를 익명으로 등록하면, 상대방도 같은 번호 쌍을 등록했을 때만 매칭이 성사된다. 연락처를 평문으로 다룰 수 없고, 상호 등록 여부는 상수 시간에 판정되어야 하며, 결제·구독·소셜 로그인은 여러 플랫폼을 수용해야 했다. 초기에는 Django 프로토타입으로 시작했지만, 비동기 API와 테스트 가능한 레이어 경계가 필요해 FastAPI + async SQLAlchemy로 재구성했다.
시스템 구조
- 도메인 경계: Router(HTTP) → Service → Repository → UnitOfWork → DB. 도메인 엔티티 10개는 dataclass로 선언해 ORM에 종속되지 않는다.
- 전화번호 이중 보호: 저장용은 AES-256-GCM으로 가역 암호화, 검색용은 SHA-256 + Pepper로 단방향 해시. 해시 컬럼에 인덱스를 걸어 상호 등록 판정을 O(1)로 처리.
- 토큰 경제:
TokenLedger가 모든 증감을 append-only로 기록. 매칭 즉시 공개와 흔적 공개는 각각 다른 슬롯에서 토큰을 차감한다. - 멱등성 결제: Toss는 구현 완료, Apple IAP·Google Play는 구조 정의 완료.
webhook_events·outbox_events·payment_ledger·entitlement_audit_logs4개 테이블로 멱등성 키 + TX ID + 금액 3중 검증을 남긴다.
해결 과정
Django → FastAPI 재구성. 동기 ORM과 라우터에 비즈니스 로직이 혼재된 구조에서, 비동기 처리와 테스트 경계가 필요해 FastAPI + async SQLAlchemy 2.0으로 옮겼다. Router는 HTTP 진입만 담당하고, Service에 도메인 로직을, Repository에 쿼리를 두어 계층별로 단위 테스트가 가능하게 만들었다.
전화번호 이중 보호. 전화번호는 평문으로 DB에 두지 않는다. 저장용 컬럼은 AES-256-GCM으로 암호화해 복호화가 필요한 로직에서만 꺼내 쓰고, 검색·매칭용 컬럼은 SHA-256에 Pepper를 섞은 단방향 해시를 별도 인덱스로 보관한다. 매칭 판정은 해시 컬럼만 조회하므로 평문에 닿지 않는다.
O(1) 상호 등록 감지. 사용자 A가 번호 X를 등록하면, X의 해시를 x_registry.target_phone_hash 인덱스에서 찾아 B가 먼저 등록했는지 확인한다. 있으면 Match(PENDING, unlock_time = now + 10h) 생성, 없으면 match_traces로 흔적만 남긴다. 해시 인덱스 덕분에 등록 규모와 무관하게 상수 시간에 끝난다.
PENDING → REVEALED 전이. 매칭 상태는 두 경로로 공개된다. (1) 배치 잡이 unlock_time이 지난 매칭들을 일괄 REVEALED로 전환 — 시간이 흐르기만 하면 자연스럽게 공개된다. (2) 사용자가 즉시 공개 토큰을 차감하면 match_repository가 단건을 즉시 REVEALED로 올린다. 두 경로 모두 같은 상태 컬럼을 갱신하므로 클라이언트는 상태 한 번만 보면 공개 여부를 안다.
UnitOfWork로 트랜잭션 무결성. 회원가입(소셜 3사 연동 포함), 결제 승인, 계정 병합처럼 여러 리포지토리를 건드리는 연산은 UnitOfWork가 SQLAlchemy 세션을 컨텍스트 매니저로 감싸 하나의 트랜잭션으로 커밋/롤백한다.
3사 결제 멱등성. Toss·Apple IAP·Google Play를 webhook_events로 수신하고, outbox_events로 재시도 큐를, payment_ledger로 최종 상태를, entitlement_audit_logs로 권한 지급 감사 로그를 남긴다. 멱등성 키·거래 ID·금액을 삼중으로 대조해 중복 지급·금액 위조를 막는다.
3사 소셜 로그인과 계정 병합. Kakao·Naver·Google을 oauth_account 추상화로 통합한다. 동일 이메일이면 자동 연동, 같은 전화번호로 SMS 인증하면 수동 연동을 제공한다.
성과
- Backend 49 엔드포인트, 18 테이블, 서비스·리포지토리 각 13개로 구성된 DDD-lite 레이어드 아키텍처.
- 전화번호 이중 보호(AES-256-GCM + SHA-256 해시) + O(1) 상호 등록 판정 + 10시간 대기 매칭 로직.
- TokenLedger 기반 토큰 경제(즉시 공개 / 흔적 공개 슬롯 분리).
- 결제 4 테이블(webhook_events / outbox_events / payment_ledger / entitlement_audit_logs)로 3사 결제 멱등성·감사 기반 구성.
- OAuth 3사 통합 + 동일 이메일 자동 병합 + SMS 인증 수동 병합.
- GitHub Actions 3개 파이프라인(CI / staging / production), EC2 스테이징 서버 운영.