소셜 핀테크 — 원장 파생 지표 · 또래 비교 · 미션
FinMate — 청년 금융 온보딩
- 기간
- 2026.04 – 2026.07
- 역할
- 풀스택 — 앱의 프론트·백엔드 단독
- 참여 인력
- 4명 — 기획 2 / 풀스택 1(본인) / 데이터 1 · 가가제작소, 하나금융그룹 × 금융감독원 2026 청년 금융인재
React 19TypeScript strictViteTailwind CSS v4Motion v12React Router 7html-to-imageJava 21 · Spring Boot 3.5PostgreSQL 16Flyway · Testcontainersfal.ai (FLUX)
GitHub에서 코드 보기 ↗서비스
20대가 금융을 미루지 않고 오늘 작은 행동 하나로 시작하게 만드는 모바일 서비스입니다.
20대는 금융관리의 필요성은 알지만 '무엇부터 해야 할지 몰라서', '어렵고 귀찮아서' 시작을 미룹니다. 필요한 건 더 많은 정보가 아니라 마찰 없는 첫 걸음입니다.
그래서 또래의 금융 행동을 구경하게 해 동기를 만들고, 지금 할 수 있는 작은 미션으로 연결하고, 하루가 끝나면 그날의 소비를 그림 한 장으로 남깁니다.
- 피드
- 분석
- 마이
- 미션
- 기록


- 데모 UI
- React 19 · TypeScript strict · Tailwind v4 · Motion v12 · 커스텀 SVG 차트
- 직접 띄워보기
cd finmate-app && npm install && npm run dev→ localhost:5173 (모바일 390×844 · 화면은 finmate-app, 서버는 finmate-api)
e2e가 화면에서 재현하는 것
- 차트 라이브러리를 쓰지 않고 SVG와 Motion으로 직접 그렸다 — 물잔 수위는 사인파 두 장의 수평 루프다
- 화면의 모든 수치는 거래 원장에서 파생된다. 예산 챌린지의 일·주·월 판정도 전부 거래에서 계산한다
위 화면은 로컬 데모 실행 결과입니다. 운영 중인 서비스가 아닙니다.
요약
- 또래 비교가 매 요청 원장 88만 행을 다시 세던 것을 사람×월 사전 집계로 접어 p50 32.5ms → 0.72ms
- 고치기 전에 먼저 재서 개인 화면은 p95 0.96ms로 문제가 아님을 확인하고, 인덱스(50MB·24.0ms) 대신 사전 집계(1.9MB·0.72ms)를 선택
- 하루의 거래에서 주인공을 판정해 그림을 만드는 비동기 파이프라인 구현 — 하루 한 장 멱등성, 3회 재시도, 응답 없는 작업 회수
- 합성 데이터셋 2,000명 887,002행을 JDBC 배치로 적재(34,490행/초)하고 원장·프로필 정합성을 통합 테스트로 고정
- 백엔드 없이 시연해야 하는 제약에서 고정 날짜·고정 시드로 화면 전체를 결정적으로 만들어 촬영 재현성 확보
- 일간·주간·월간 전환 시 화면 전체가 같은 기간으로 일관되게 바뀌도록 지표를 전부 거래 원장에서 파생하는 순수 셀렉터로 통일
- 차트 라이브러리 없이 SVG와 스프링 모션으로 물잔·링 게이지·라인차트를 직접 구현해 번들에 차트 의존성 0개
- 공유 카드를 layoutId 전환으로 9:16까지 확대하고 html-to-image로 1080×1920 PNG 저장 · Web Share 연동
01청년 금융 · 인구 집계
또래 비교가 매 요청 원장 88만 행을 다시 세던 것을 사람×월 사전 집계로 접어 p50 32.5ms → 0.72ms
문제 원인
- 또래 비교는 소득대가 같은 사람 전부를 가로질러 집계하므로 한 달치가 105,484행
- 조건이 기간뿐이라 (persona_id, occurred_on) 인덱스의 선행 컬럼이 없어 Parallel Seq Scan
- count(DISTINCT persona_id)가 정렬을 강요해 work_mem을 넘기고 디스크 정렬 2,496kB 발생
해결 과정
- 고치기 전에 먼저 측정 — 개인 화면도 함께 재보니 p95 0.96ms로 문제가 아니었고, 예상과 달리 인구 집계만 비쌌음
- 싼 것부터 순서대로 검증 — 쿼리 수정(count DISTINCT 제거)으로 디스크 정렬을 없애 p50 28.0ms
- 커버링 인덱스 (flow, occurred_on) INCLUDE (persona_id, amount)는 50MB를 쓰고 24.0ms에 그쳐 채택하지 않음
- 사람×월 사전 집계를 도입하되 원장을 유일한 진실로 두어 언제든 통째로 재생성 가능하게 설계
결과
- p50 32.5ms → 0.72ms, 읽는 버퍼 23,326 → 206 (전체 2,000명 · 원장 887,002행 기준)
- 사전 집계는 14,000행 1.9MB로 원장의 1.1%, 전체 재생성 401ms
- 두 방식의 결과가 소득대 6개 그룹 전부 일치하는 것을 통합 테스트로 고정
02AI 그림일기 · 외부 의존 비동기
행 잠금이 외부 호출 구간을 지키지 못하던 것을 상태 전이로 집는 방식으로 바꿔 두 인스턴스가 같은 작업을 가져가지 못하게 함
문제 원인
- 하루의 소비를 그림 한 장으로 만드는 기능이라 외부 이미지 생성 API 호출에 3~6초가 걸림
- 처음엔 SELECT … FOR UPDATE SKIP LOCKED로 집고 별도 트랜잭션에서 외부 API를 불렀는데, 잠금은 그 트랜잭션이 끝나며 풀리므로 정작 호출이 도는 동안에는 아무도 그 행을 지키지 않음
- 그렇다고 잠근 채로 부르면 3~6초 동안 DB 커넥션을 붙들게 되어 커넥션 풀이 마름
해결 과정
- 잠금이 아니라 상태로 집기 — PENDING을 SUBMITTED로 바꾸는 UPDATE … RETURNING 한 문장이 원자적이라 두 인스턴스가 같은 행을 가져갈 수 없음
- SKIP LOCKED는 서로 다른 행을 동시에 집을 때 기다리지 않게 하는 역할로만 남김
- 집은 직후 죽으면 job id 없는 SUBMITTED가 남으므로, 일정 시간이 지난 것은 PENDING으로 되돌리는 회수 경로를 따로 둠
- 수거 대상은 job id가 있는 SUBMITTED로 한정 — 제출 전에 죽은 행까지 수거하면 멀쩡한 작업을 실패로 버리게 됨
결과
- 동시에 요청해도 그림이 하나만 생기는 것을 Testcontainers 실 PostgreSQL 통합 테스트로 고정
- (persona_id, entry_date) 유니크 제약으로 같은 날을 두 번 요청해도 그림은 하나
- 실패는 세 번까지 재시도하고 이유를 남기며, 응답이 오지 않는 작업은 되돌려 다시 집게 함
구현 기능
위 문제 해결 외에, 서비스가 돌아가기 위해 구현한 것들입니다.
- 하루의 거래에서 그날의 주인공(소비·저축·투자)을 뽑아 AI 그림일기 한 장으로 기록 — 그림체가 날마다 달라진다
- 예산 챌린지 판정을 저장하지 않고 매번 원장에서 세고, 포인트는 잔액이 아니라 원장으로 쌓아 이중 지급 차단
- 또래 비교에서 금액을 구간으로만 내보내고 20명 미만 그룹은 만들지 않음
- 또래 그룹 탐색(소득·소비·지역·팔로잉 필터)과 오늘의 스토리 그리드
- 포인트 상점과 예산 챌린지, 진행·추천 미션
- 습관 기반 6개월 투영 그래프와 메이트·그룹 선 겹치기
수치를 싣지 않은 이유
동시 사용자 부하는 측정하지 않았습니다. 위 수치는 단일 클라이언트가 순차로 호출해 잰 값이라 처리량 지표가 아닙니다. 화면이 이 백엔드에 붙는 작업은 일부만 됐습니다 — 2026-08-07에 둘을 함께 띄워 확인해 보니, 마이 탭의 기준일·예산·소비 상위는 실제 원장에서 파생돼 화면까지 오지만 피드·분석·미션·기록 네 탭은 아직 목 데이터로 그립니다(탭을 옮겨도 API를 부르지 않습니다). 또래 비교·미션·인사이트 API는 만들어 두었지만 앱이 아직 부르지 않으므로, 끝에서 끝까지의 응답 시간은 주장하지 않습니다. 위 화면 사진 두 장도 목 데이터로 찍은 것입니다.