Project
머니가드
금전 거래를 한 곳에서 기록하고, 차용증 기반으로 상환을 관리할 수 있도록 지원하는 플랫폼
역할
풀스택 개발
기간
2025.12 ~ 2026.03
스택
Python, FastAPI, PostgreSQL, Redis
배경 및 문제
기존 머니가드는 모바일 앱 전용 서비스였습니다. 앱에서는 카카오 SDK를 통해 클라이언트가 직접 소셜 토큰을 받아 서버에 전달하는 방식을 사용했고, 이는 모바일 환경에서의 구조였습니다.
서비스가 웹으로 확장되면서 문제가 생겼습니다. 웹에는 네이티브 SDK가 없어 같은 방식을 그대로 쓸 수 없었고, 클라이언트가 소셜 정보를 직접 서버에 전달하는 구조가 갖는 보안 취약점도 함께 드러났습니다.
- 소셜 토큰 서버 검증 없음 — 클라이언트가 전달하는
social_id를 서버가 그대로 신뢰 - 임의 social_id 위조 가능 — 요청 바디 조작으로 타인 계정 접근 가능성
- 개인정보 클라이언트 경유 — 이름, 전화번호가 요청 바디에 평문 노출
해결 방향
상세 내용: SNS 로그인 서버 중심으로 리팩토링
앱과 웹 모두에서 동작하는 서버 중심 OAuth 2.0 플로우를 신규 v2 API로 설계했습니다. 기존 앱 클라이언트는 수정 없이 계속 동작해야 했기 때문에, 레거시 API를 무영향으로 유지하면서 v2를 병렬로 추가하는 방식을 선택했습니다.
기존의 클라이언트 중심 방식

변경 후 서버 중심 방식

핵심 기술 결정
1. 레거시 Callback 재사용
v2 전용 callback 엔드포인트를 새로 만드는 대신, 기존 엔드포인트에 3줄 분기만 추가했습니다. 카카오 OAuth는 authorize_url과 token 교환의 redirect_uri가 정확히 일치해야 하기 때문에, v2 전용 URL을 만들면 카카오 개발자 콘솔 신규 등록이 필요합니다. state에 v2_ prefix로 분기하면 기존 설정을 그대로 재사용할 수 있었습니다.
2. Redis GETDEL — 원자적 1회성 소비
GET 후 DELETE는 동시 요청 시 두 번 모두 성공할 수 있는 race condition이 있습니다. Redis GET + DEL을 단일 원자 연산으로 처리해 state/code 중복 소비를 원천 차단했습니다.
3. redirect_uri Allowlist 정규화
단순 문자열 비교는 https://example.com/ vs https://example.com 같은 우회가 가능합니다. URL을 scheme/host/port/path 기준으로 정규화 후 exact match하여 Open Redirect 공격을 차단했습니다.
4. Redis Namespace 분리
레거시와 v2의 Redis key를 완전히 분리(oauth:state:* vs oauth:v2:state:*)해 상호 간섭을 차단했습니다.
5. PendingUser Partial Unique Index
신규 회원 처리 중 동일 SNS 계정의 pending row가 중복 생성되지 않도록, PostgreSQL partial unique index + upsert를 활용했습니다. 충돌 시 본인인증 진행 중인 세션 데이터는 절대 reset하지 않도록 설계했습니다.
성과
- 앱/웹 통합 지원: 서버 중심 플로우로 앱과 웹 모두에서 동일한 인증 방식 사용 가능
- 레거시 무영향 배포: 기존 앱 클라이언트 수정 없이 신규 플로우 병렬 운영
- 동시성 안전: Redis GETDEL로 state/code 중복 소비 원천 차단
- Open Redirect 방어: redirect_uri 정규화 + allowlist exact match 구현
- 정보 최소 노출: 에러 응답에서 upstream raw body 완전 차단
결과
앱 → 앱/웹 확장 중 SNS 로그인 서버 중심으로 전환하여 보안 취약점 해결