Database Schema

PostgreSQL 16 · 44개 테이블 · 파티셔닝 적용 · 대용량 매칭 최적화

44
테이블
67
인덱스
12
복합 인덱스
1
파티션 테이블

전체 구조

erDiagram users ||--|| profiles : "1:1" users ||--o{ user_photos : "1:N" users ||--o{ likes : "전송" users ||--o{ matches : "참여" users ||--o{ payments : "결제" matches ||--o{ messages : "포함" users ||--o{ blocks : "차단" users ||--o{ reports : "신고" admins ||--o{ admin_logs : "기록"

사용자 & 프로필

users

핵심 사용자 테이블. 카드 추천 쿼리 최적화를 위해 자주 조회되는 필드를 비정규화.

컬럼타입제약조건설명
idUUIDPKgen_random_uuid()
emailVARCHAR(255)UNIQUE선택사항
phoneVARCHAR(20)UNIQUE NOT NULLPASS 본인인증
nicknameVARCHAR(20)UNIQUE NOT NULL
genderVARCHAR(10)NOT NULLMALE, FEMALE
birth_dateDATENOT NULL
region_cityVARCHAR(50)시/도
region_districtVARCHAR(50)구/군
pointsINTEGERDEFAULT 0보유 포인트
statusVARCHAR(20)DEFAULT 'ACTIVE'ACTIVE, SUSPENDED, DELETED
heightINTEGER비정규화 profiles에서 복제
is_vipBOOLEANDEFAULT FALSE비정규화
last_active_atTIMESTAMP비정규화

인덱스

이름컬럼타입용도
idx_users_card_optimized gender, status, region_city, birth_date, height 복합 부분 카드 추천 - profiles JOIN 제거
idx_users_card_vip gender, status, is_vip DESC, last_active_at DESC 복합 VIP 우선 정렬
idx_users_region_age region_city, region_district, birth_date 복합 지역 + 나이 필터 (가장 빈번)
idx_users_gender gender B-tree 성별 필터
idx_users_status status 부분 WHERE status = 'ACTIVE'
비정규화 전략: height, is_vip, last_active_at을 users 테이블에 복제. 카드 추천 쿼리에서 profiles JOIN을 제거하여 응답시간 ~200ms → ~15ms로 단축.

profiles

컬럼타입제약조건
user_idUUIDFK → users, UNIQUE
heightINTEGERCHECK (140-220)
body_typeVARCHAR(20)SLIM, NORMAL, CHUBBY, MUSCULAR
mbtiVARCHAR(4)
religionVARCHAR(20)NONE, CHRISTIAN, CATHOLIC, BUDDHIST, OTHER
smokingVARCHAR(20)NO, YES, QUITTING
drinkingVARCHAR(20)NO, SOMETIMES, OFTEN
jobVARCHAR(50)
introductionTEXT

매칭 시스템

erDiagram likes { uuid id PK uuid from_user_id FK uuid to_user_id FK varchar status int retry_count timestamp can_retry_at } passes { uuid id PK uuid from_user_id FK uuid to_user_id FK timestamp expires_at int pass_count } matches { uuid id PK uuid user1_id FK uuid user2_id FK varchar status timestamp can_rematch_at } messages { uuid id PK uuid match_id FK uuid sender_id FK text content timestamp created_at } likes ||--o| matches : "생성" matches ||--o{ messages : "포함"

likes

재신청 메커니즘 지원 - 거절된 좋아요도 쿨다운 후 재시도 가능.

컬럼타입설명
statusVARCHAR(20)PENDING → MATCHED / REJECTED / EXPIRED
retry_countINTEGER최대 3회 재신청 허용
can_retry_atTIMESTAMP거절: +30일, 만료: 즉시
expires_atTIMESTAMP생성 후 30일

인덱스

이름타입용도
idx_likes_active Unique 부분 UNIQUE(from_user_id, to_user_id) WHERE status = 'PENDING'
idx_likes_from_to 복합 이미 좋아요한 사용자 카드에서 제외
idx_likes_mutual 복합 (to_user_id, from_user_id) - 상호 좋아요 확인
idx_likes_expires 부분 배치: 만료된 좋아요 처리

passes

점진적 쿨다운: 1회 패스 = 7일, 2회 = 14일, 3회 이상 = 30일

matches

컬럼타입설명
user1_id, user2_idUUIDCHECK (user1_id < user2_id) - 중복 방지
statusVARCHAR(20)ACTIVE, UNMATCHED, BLOCKED
can_rematch_atTIMESTAMP언매치: +90일, 차단: 영구

messages 파티셔닝

created_at 기준 월별 Range 파티셔닝. 대용량 트래픽에서 쿼리 성능 핵심.

CREATE TABLE messages (
    id UUID NOT NULL,
    match_id UUID NOT NULL,
    sender_id UUID NOT NULL,
    content TEXT NOT NULL,
    created_at TIMESTAMP NOT NULL,
    PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);

-- 월별 파티션
CREATE TABLE messages_2025_01 PARTITION OF messages
    FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
CREATE TABLE messages_2025_02 PARTITION OF messages
    FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');
-- ...
효과적용 전적용 후
쿼리 스캔전체 테이블단일 파티션만
인덱스 크기단일 대형 인덱스파티션별 소형 인덱스
데이터 삭제DELETE (느림)DROP PARTITION (즉시)
백업전체 백업 필수월별 증분 백업 가능

결제 시스템

payments

컬럼타입설명
idempotency_keyVARCHAR(64)UNIQUE NOT NULL 중복 결제 방지
product_typeVARCHAR(20)POINT, PASS, VIP
platformVARCHAR(20)IOS, ANDROID, WEB
transaction_idVARCHAR(255)스토어 거래 ID
statusVARCHAR(20)PENDING, COMPLETED, CANCELLED, REFUNDED
멱등성 처리: 클라이언트가 결제 시도마다 UUID v4 생성. 동일한 idempotency_key로 재요청 시 새 결제 없이 기존 결과 반환. 키는 24시간 후 만료.

point_history

모든 포인트 거래 감사 로그. balance 컬럼에 거래 후 잔액 저장하여 빠른 조회.

컬럼타입설명
amountINTEGER양수: 적립, 음수: 차감
balanceINTEGER거래 후 잔액
typeVARCHAR(30)PURCHASE, LIKE_SEND, SUPER_LIKE, BOOST, REFUND, ADMIN, EXCHANGE
reference_idUUID관련 결제/좋아요/부스트 ID

vip_subscriptions

컬럼타입설명
tierVARCHAR(20)BRONZE, SILVER, GOLD
auto_renewBOOLEAN스토어 구독 상태
subscription_idVARCHAR(255)스토어 구독 ID (관리용)

인덱스

이름용도
idx_payments_idempotency멱등성 키 조회 (유니크 제약)
idx_vip_activeWHERE status = 'ACTIVE' - VIP 상태 확인
idx_vip_expiring배치: 갱신 알림 발송
idx_point_history_user_created(user_id, created_at DESC) - 내역 조회

관리자 & 통계

featured_users

인기/추천 회원 시스템. 자동(알고리즘) + 수동(관리자) 선정 지원.

컬럼타입설명
feature_typeVARCHAR(30)POPULAR (자동), RECOMMENDED (수동), NEW_FACE, VERIFIED_PREMIUM
is_autoBOOLEAN알고리즘 선정 vs 관리자 선정
priorityINTEGER높을수록 더 많이 노출
target_genderVARCHAR(10)특정 성별에게만 노출
impressionsINTEGER성과 추적

user_scores

랭킹 및 추천 알고리즘용 계산 지표.

컬럼타입갱신 주기
popularity_scoreINTEGER일일 배치
likes_received_7dINTEGER일일 배치
response_rateDECIMAL(5,2)메시지마다
trust_scoreINTEGER신고 처리 시
profile_scoreINTEGER프로필 수정 시

daily_statistics / monthly_statistics

대시보드용 사전 집계 지표. 야간 배치로 계산.

지표설명
DAU / MAU일간/월간 활성 사용자
new_users / churned_users성장 지표
total_revenue / ARPU / ARPPU매출 지표
conversion_rate무료 → 유료 전환율

쿼리 최적화

카드 추천 쿼리

가장 중요한 쿼리 - 스와이프할 때마다 실행. 목표: 10만 MAU 기준 20ms 이하.

-- 최적화 전: ~200ms (풀 테이블 스캔 + JOIN)
SELECT u.*, p.* FROM users u
JOIN profiles p ON u.id = p.user_id
WHERE u.gender = 'FEMALE' AND u.status = 'ACTIVE'
  AND u.id NOT IN (SELECT to_user_id FROM likes WHERE from_user_id = $1)
  AND u.id NOT IN (SELECT blocked_id FROM blocks WHERE blocker_id = $1)
ORDER BY u.last_login_at DESC LIMIT 10;

-- 최적화 후: ~15ms (인덱스 스캔, JOIN 없음)
SELECT * FROM users
WHERE gender = $1 AND status = 'ACTIVE'
  AND region_city = $2
  AND birth_date BETWEEN $3 AND $4
  AND id != ALL($5::uuid[])  -- Redis에서 가져온 제외 ID
ORDER BY is_vip DESC, last_active_at DESC
LIMIT 10;

적용된 최적화 기법

Hot Field 비정규화
height, is_vip, last_active_at을 users 테이블에 복제. profiles JOIN 제거.
Redis 제외 캐시
본/좋아요/차단 사용자 ID를 Redis SET에 캐싱. 서브쿼리 대신 배열 파라미터로 전달.
복합 인덱스
WHERE + ORDER BY 절에 정확히 매칭되는 다중 컬럼 인덱스.
부분 인덱스
WHERE status = 'ACTIVE' 조건으로 인덱스 크기 ~30% 감소.

Redis 캐싱 전략

키 패턴타입TTL용도
user:{id}:seenSET24시간오늘 본 카드
user:{id}:likes_sentSET7일카드에서 제외
user:{id}:blockedSET영구차단한 사용자
user:{id}:profileHASH1시간프로필 캐시
user:{id}:pointsSTRING5분포인트 잔액

재시도 & 재매칭 정책

시나리오쿨다운최대 재시도테이블
패스 (카드 넘기기)7일 → 14일 → 30일무제한passes
좋아요 만료즉시3회likes
좋아요 거절30일3회likes
언매치90일무제한matches
차단영구불가blocks

테이블 관계 상세

사용자 도메인

erDiagram users ||--|| profiles : "1:1 프로필 정보" users ||--o{ user_photos : "1:N 최대 6장" users ||--o{ social_accounts : "1:N 소셜 연동" users ||--o{ verifications : "1:N 추가 인증" users ||--o{ profile_tags : "1:N 태그" users ||--|| user_preferences : "1:1 필터 설정" users ||--o{ device_tokens : "1:N 멀티 디바이스" users ||--o{ refresh_tokens : "1:N 세션 관리" users ||--|| notification_settings : "1:1 알림 설정" users { uuid id PK varchar phone UK varchar nickname UK varchar gender date birth_date int points } profiles { uuid user_id PK,FK int height varchar mbti text introduction } user_photos { uuid id PK uuid user_id FK boolean is_main int display_order }

매칭 플로우

flowchart LR A[카드 노출] --> B{사용자 액션} B -->|좋아요| C[likes 생성] B -->|패스| D[passes 생성] B -->|슈퍼좋아요| E[likes + type=SUPER] C --> F{상대방 반응} F -->|좋아요| G[matches 생성] F -->|거절| H[likes.status=REJECTED] F -->|무응답 30일| I[likes.status=EXPIRED] G --> J[messages 교환] J --> K{관계 종료} K -->|언매치| L[90일 후 재매칭 가능] K -->|차단| M[영구 차단] D --> N[7/14/30일 후 재노출] H --> O[30일 후 재신청 가능] I --> P[즉시 재신청 가능]

결제 도메인

erDiagram users ||--o{ payments : "결제" users ||--o{ point_history : "포인트 내역" users ||--o| vip_subscriptions : "VIP 구독" users ||--o{ user_passes : "이용권" users ||--o{ user_coupons : "쿠폰 보유" coupons ||--o{ user_coupons : "발급" payments ||--o| refund_requests : "환불" users ||--o{ point_exchanges : "포인트 전환" payments { uuid id PK uuid user_id FK varchar idempotency_key UK int amount varchar status } point_history { uuid id PK uuid user_id FK int amount int balance varchar type } vip_subscriptions { uuid id PK uuid user_id FK varchar tier timestamp end_date }

포인트 흐름

flowchart TB subgraph 적립 A1[결제] --> P[points] A2[쿠폰 사용] --> P A3[추천인 보상] --> P A4[관리자 지급] --> P A5[환불] --> P end subgraph 차감 P --> B1[좋아요 10P] P --> B2[슈퍼좋아요 30P] P --> B3[프로필 확인 20P] P --> B4[부스트 50P] P --> B5[포인트 전환] end P --> H[point_history 기록]

운영 전략

백업 정책

구분방식주기보관 기간
전체 백업pg_dump (RDS 스냅샷)일 1회 (03:00)30일
증분 백업WAL 아카이브실시간7일
messages 파티션월별 S3 아카이브월 1회1년
point_history별도 백업일 1회5년 (법적 요구)

데이터 보존 정책

데이터보존 기간처리 방식
탈퇴 회원 개인정보즉시 삭제users.status = 'DELETED', 개인정보 null 처리
탈퇴 회원 거래내역5년payments, point_history 유지 (법적 요구)
채팅 메시지1년월별 파티션 DROP
카드 조회 기록90일card_history 배치 삭제
알림30일notifications 배치 삭제
관리자 로그2년admin_logs 아카이브

스케일링 전략

Read Replica
카드 추천, 프로필 조회 등 읽기 트래픽은 Read Replica로 분산.
쓰기는 Primary로만. 복제 지연 ~100ms 허용.
Connection Pooling
PgBouncer 사용. Transaction 모드.
max_connections: 100 → 실제 동시 처리 1000+
테이블 파티셔닝
messages: 월별 Range 파티션.
향후 likes, card_history도 파티셔닝 고려.
캐시 계층
Redis: 세션, 제외 목록, 포인트 잔액.
CDN: 프로필 이미지, 정적 리소스.

장애 대응

장애 유형감지대응RTO
DB Primary 장애 RDS 자동 감지 Multi-AZ 자동 failover < 2분
Redis 장애 Health check 실패 ElastiCache 자동 failover, 앱은 DB fallback < 1분
디스크 풀 CloudWatch 알람 (80%) 오래된 파티션 아카이브, 스토리지 확장 수동 대응
슬로우 쿼리 APM (p99 > 500ms) 쿼리 분석, 인덱스 추가, 캐시 적용 수동 대응
Connection 고갈 연결 수 모니터링 PgBouncer 풀 확장, 비정상 연결 kill < 5분

모니터링 지표

지표정상 범위알람 조건
CPU 사용률< 60%> 80% 5분 지속
메모리 사용률< 70%> 85%
디스크 사용률< 70%> 80%
Connection 수< 80> 90
복제 지연< 100ms> 1초
카드 추천 API p99< 50ms> 200ms
트랜잭션/초기준선 ±20%급격한 변화

배치 작업

작업스케줄대상 테이블예상 소요
만료된 좋아요 처리매일 04:00likes~5분
패스 만료 처리매일 04:10passes~3분
VIP 만료 처리매일 04:20vip_subscriptions, users~2분
일일 통계 집계매일 05:00daily_statistics~10분
인기회원 갱신매일 06:00user_scores, featured_users~15분
오래된 알림 삭제매일 03:00notifications~5분
파티션 생성매월 1일messages즉시
월간 통계 집계매월 1일 07:00monthly_statistics~30분

전체 테이블 목록

사용자 & 프로필 (10)
users, profiles, user_photos, social_accounts, verifications, profile_tags, user_preferences, device_tokens, refresh_tokens, notification_settings
매칭 (8)
likes, passes, matches, messages, profile_views, card_history, boosts, user_daily_limits
결제 (10)
payments, point_history, vip_subscriptions, user_passes, coupons, user_coupons, products, point_exchanges, point_exchange_config, refund_requests
소셜 (7)
blocks, contact_blocks, reports, notifications, referral_codes, referrals, referral_config
관리자 (9)
admins, admin_logs, watched_users, featured_users, user_scores, notices, faqs, daily_statistics, monthly_statistics