Database Schema
PostgreSQL 16 · 44개 테이블 · 파티셔닝 적용 · 대용량 매칭 최적화
전체 구조
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
핵심 사용자 테이블. 카드 추천 쿼리 최적화를 위해 자주 조회되는 필드를 비정규화.
| 컬럼 | 타입 | 제약조건 | 설명 |
id | UUID | PK | gen_random_uuid() |
email | VARCHAR(255) | UNIQUE | 선택사항 |
phone | VARCHAR(20) | UNIQUE NOT NULL | PASS 본인인증 |
nickname | VARCHAR(20) | UNIQUE NOT NULL | |
gender | VARCHAR(10) | NOT NULL | MALE, FEMALE |
birth_date | DATE | NOT NULL | |
region_city | VARCHAR(50) | | 시/도 |
region_district | VARCHAR(50) | | 구/군 |
points | INTEGER | DEFAULT 0 | 보유 포인트 |
status | VARCHAR(20) | DEFAULT 'ACTIVE' | ACTIVE, SUSPENDED, DELETED |
height | INTEGER | | 비정규화 profiles에서 복제 |
is_vip | BOOLEAN | DEFAULT FALSE | 비정규화 |
last_active_at | TIMESTAMP | | 비정규화 |
인덱스
| 이름 | 컬럼 | 타입 | 용도 |
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_id | UUID | FK → users, UNIQUE |
height | INTEGER | CHECK (140-220) |
body_type | VARCHAR(20) | SLIM, NORMAL, CHUBBY, MUSCULAR |
mbti | VARCHAR(4) | |
religion | VARCHAR(20) | NONE, CHRISTIAN, CATHOLIC, BUDDHIST, OTHER |
smoking | VARCHAR(20) | NO, YES, QUITTING |
drinking | VARCHAR(20) | NO, SOMETIMES, OFTEN |
job | VARCHAR(50) | |
introduction | TEXT | |
매칭 시스템
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
재신청 메커니즘 지원 - 거절된 좋아요도 쿨다운 후 재시도 가능.
| 컬럼 | 타입 | 설명 |
status | VARCHAR(20) | PENDING → MATCHED / REJECTED / EXPIRED |
retry_count | INTEGER | 최대 3회 재신청 허용 |
can_retry_at | TIMESTAMP | 거절: +30일, 만료: 즉시 |
expires_at | TIMESTAMP | 생성 후 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_id | UUID | CHECK (user1_id < user2_id) - 중복 방지 |
status | VARCHAR(20) | ACTIVE, UNMATCHED, BLOCKED |
can_rematch_at | TIMESTAMP | 언매치: +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_key | VARCHAR(64) | UNIQUE NOT NULL 중복 결제 방지 |
product_type | VARCHAR(20) | POINT, PASS, VIP |
platform | VARCHAR(20) | IOS, ANDROID, WEB |
transaction_id | VARCHAR(255) | 스토어 거래 ID |
status | VARCHAR(20) | PENDING, COMPLETED, CANCELLED, REFUNDED |
멱등성 처리: 클라이언트가 결제 시도마다 UUID v4 생성. 동일한 idempotency_key로 재요청 시 새 결제 없이 기존 결과 반환. 키는 24시간 후 만료.
point_history
모든 포인트 거래 감사 로그. balance 컬럼에 거래 후 잔액 저장하여 빠른 조회.
| 컬럼 | 타입 | 설명 |
amount | INTEGER | 양수: 적립, 음수: 차감 |
balance | INTEGER | 거래 후 잔액 |
type | VARCHAR(30) | PURCHASE, LIKE_SEND, SUPER_LIKE, BOOST, REFUND, ADMIN, EXCHANGE |
reference_id | UUID | 관련 결제/좋아요/부스트 ID |
vip_subscriptions
| 컬럼 | 타입 | 설명 |
tier | VARCHAR(20) | BRONZE, SILVER, GOLD |
auto_renew | BOOLEAN | 스토어 구독 상태 |
subscription_id | VARCHAR(255) | 스토어 구독 ID (관리용) |
인덱스
| 이름 | 용도 |
idx_payments_idempotency | 멱등성 키 조회 (유니크 제약) |
idx_vip_active | WHERE status = 'ACTIVE' - VIP 상태 확인 |
idx_vip_expiring | 배치: 갱신 알림 발송 |
idx_point_history_user_created | (user_id, created_at DESC) - 내역 조회 |
관리자 & 통계
featured_users
인기/추천 회원 시스템. 자동(알고리즘) + 수동(관리자) 선정 지원.
| 컬럼 | 타입 | 설명 |
feature_type | VARCHAR(30) | POPULAR (자동), RECOMMENDED (수동), NEW_FACE, VERIFIED_PREMIUM |
is_auto | BOOLEAN | 알고리즘 선정 vs 관리자 선정 |
priority | INTEGER | 높을수록 더 많이 노출 |
target_gender | VARCHAR(10) | 특정 성별에게만 노출 |
impressions | INTEGER | 성과 추적 |
user_scores
랭킹 및 추천 알고리즘용 계산 지표.
| 컬럼 | 타입 | 갱신 주기 |
popularity_score | INTEGER | 일일 배치 |
likes_received_7d | INTEGER | 일일 배치 |
response_rate | DECIMAL(5,2) | 메시지마다 |
trust_score | INTEGER | 신고 처리 시 |
profile_score | INTEGER | 프로필 수정 시 |
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}:seen | SET | 24시간 | 오늘 본 카드 |
user:{id}:likes_sent | SET | 7일 | 카드에서 제외 |
user:{id}:blocked | SET | 영구 | 차단한 사용자 |
user:{id}:profile | HASH | 1시간 | 프로필 캐시 |
user:{id}:points | STRING | 5분 | 포인트 잔액 |
재시도 & 재매칭 정책
| 시나리오 | 쿨다운 | 최대 재시도 | 테이블 |
| 패스 (카드 넘기기) | 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:00 | likes | ~5분 |
| 패스 만료 처리 | 매일 04:10 | passes | ~3분 |
| VIP 만료 처리 | 매일 04:20 | vip_subscriptions, users | ~2분 |
| 일일 통계 집계 | 매일 05:00 | daily_statistics | ~10분 |
| 인기회원 갱신 | 매일 06:00 | user_scores, featured_users | ~15분 |
| 오래된 알림 삭제 | 매일 03:00 | notifications | ~5분 |
| 파티션 생성 | 매월 1일 | messages | 즉시 |
| 월간 통계 집계 | 매월 1일 07:00 | monthly_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