Skip to content

Repository files navigation

Reboot Market

중고 거래 플랫폼 — MSA(Microservices Architecture) 기반으로 설계된 풀스택 중고 거래 서비스입니다. Docker Compose 한 명령으로 전체 시스템을 기동할 수 있습니다.

운영 환경 (AWS EKS) 아키텍처 설계는 infra.md, 단계별 배포 런북은 eks-deploy.md 를 참고하세요. CI/CD 브랜치 전략, 파이프라인, 배포 흐름은 cicd.md 를 참고하세요.


핵심 특징

  • MSA · DB per Service — 도메인별 PostgreSQL 4개 인스턴스로 분리, 서비스 간 DB 직접 공유 없음
  • 분산 트랜잭션 — 거래 제안 수락을 2-Phase + 보상 트랜잭션으로 처리해 서비스 간 정합성 확보
  • 데이터 비정규화 + 동기화 — cross-domain JOIN 제거용 생성 시점 스냅샷, 변경 시 내부 HTTP로 동기화
  • 실시간 채팅 — WebSocket + Redis pub/sub, 서비스 수평 확장에도 메시지 브로드캐스트
  • 분산 추적 — OpenTelemetry + Jaeger로 요청 하나의 서비스 fan-out을 end-to-end 추적
  • 전문 검색 — PostgreSQL FTS(GIN 인덱스) + Redis Sorted Set 기반 실시간 인기 검색어
  • 2-tier 배포 — 로컬은 Docker Compose, 운영은 AWS EKS (Istio · Karpenter · ArgoCD GitOps)

서비스 아키텍처

동일한 MSA 구성을 로컬은 Docker Compose, 운영은 AWS EKS 두 타깃으로 배포합니다.

로컬 (Docker Compose)

Reboot Market 서비스 아키텍처 (Docker Compose)

Mermaid 다이어그램
flowchart TB
    Browser(["클라이언트\n브라우저"])

    subgraph AWS["AWS"]
        direction LR
        CF["CloudFront CDN\n이미지 서빙"]
        S3[("S3\nreboot-images")]
        CF --- S3
    end

    subgraph Platform["Docker Compose"]
        direction TB

        GW["Nginx  :3333\nAPI Gateway"]

        subgraph FrontEnd["프론트엔드"]
            FE["Next.js 14\n:3000"]
        end

        subgraph Services["백엔드 서비스  ·  FastAPI  ·  Python 3.12"]
            direction LR
            US["user-service\n:3001\n회원·JWT·인증"]
            LS["listing-service\n:3002\n상품 CRUD·이미지"]
            OS["offer-service\n:3003\n제안·수락·거절"]
            SS["search-service\n:3004\nFTS·인기검색어"]
            CS["chat-service\n:3006\n실시간 채팅\nWebSocket+Redis pub/sub"]
            AS["admin-service\n:3005\n대시보드·관리\ndual DB pool"]
        end

        subgraph DataStore["데이터 저장소"]
            direction LR
            PGU[("postgres-user\n:5433\nreboot_user")]
            PGL[("postgres-listing\n:5434\nreboot_listing")]
            PGO[("postgres-offer\n:5435\nreboot_offer")]
            PGC[("postgres-chat\n:5436\nreboot_chat")]
            RD[("Redis 7\n:6379\n캐시 · pub/sub\n인기검색어")]
        end

        subgraph Observability["관측성"]
            JAE["Jaeger\n:16686\n분산 추적 UI"]
        end
    end

    %% ── 클라이언트 진입 ──────────────────────────────────────────
    Browser -- "페이지 · API 호출" --> GW
    Browser -- "이미지 로드" --> CF
    Browser -. "직접 PUT 업로드\n(presigned URL)" .-> S3

    %% ── API Gateway 라우팅 ───────────────────────────────────────
    GW -- "/*" --> FE
    GW -- "/api/auth\n/api/users" --> US
    GW -- "/api/listings" --> LS
    GW -- "/api/offers" --> OS
    GW -- "/api/search" --> SS
    GW -- "/api/chat\n/ws/chat (WebSocket)" --> CS
    GW -- "/api/admin" --> AS

    %% ── DB / Cache ───────────────────────────────────────────────
    US --- PGU
    US --- RD
    LS --- PGL
    LS --- RD
    OS --- PGO
    OS --- RD
    SS --- PGL
    SS --- RD
    CS --- PGC
    CS --- RD
    AS --- PGU
    AS --- PGL

    %% ── 이미지 스토리지 ──────────────────────────────────────────
    LS -. "presigned URL 발급\n(boto3)" .-> S3

    %% ── 서비스 간 내부 HTTP (JWT 없음, Docker 내부망) ───────────
    OS -- "GET  /internal/listings/{id}\nPATCH /internal/listings/{id}/status" --> LS
    OS -- "GET  /internal/users/{id}" --> US
    OS -- "POST /internal/chat/rooms" --> CS
    CS -- "GET  /internal/listings/{id}" --> LS
    CS -- "GET  /internal/users/{id}" --> US
    US -- "PATCH /internal/listings/seller-nickname" --> LS
    AS -- "GET  /internal/offers/stats\n        today · activity · trend" --> OS

    %% ── 분산 추적 ────────────────────────────────────────────────
    US & LS & OS & SS & CS & AS -. "OTel  OTLP  :4317" .-> JAE

    %% ── 스타일 ───────────────────────────────────────────────────
    classDef svcStyle  fill:#dbeafe,stroke:#3b82f6,color:#1e3a8a,rx:6
    classDef dbStyle   fill:#fef9c3,stroke:#ca8a04,color:#713f12
    classDef awsStyle  fill:#fce7f3,stroke:#db2777,color:#831843
    classDef gwStyle   fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef obsStyle  fill:#f3e8ff,stroke:#9333ea,color:#4c1d95

    class US,LS,OS,SS,CS,AS svcStyle
    class PGU,PGL,PGO,PGC,RD dbStyle
    class CF,S3 awsStyle
    class GW,FE gwStyle
    class JAE obsStyle
Loading

운영 (AWS EKS)

Route 53 → ALB (TLS 종료) → Istio Ingress Gateway → EKS 서비스 Pod 구조로, Karpenter 노드 오토스케일링과 ArgoCD GitOps 배포를 사용합니다. 리소스 구성·설계 의도는 infra.md, 단계별 배포 절차(Terraform → Helm 런북)는 eks-deploy.md 를 참고하세요.

Reboot Market 운영 아키텍처 (AWS EKS)

이미지 업로드 흐름 (presigned URL)

① 클라이언트  →  listing-service   POST /api/listings/presigned-url
② listing-service  →  S3 (boto3)   presigned PUT URL 생성 (TTL 1h)
③ 클라이언트  →  S3                이미지 직접 PUT 업로드
④ 클라이언트  →  listing-service   POST /api/listings/{id}/images  (object_key 저장)
⑤ 화면 표시   ←  CloudFront        https://{CLOUDFRONT_DOMAIN}/{object_key}

서비스 간 내부 HTTP 통신 요약

호출자 피호출자 엔드포인트 목적
offer-service listing-service GET /internal/listings/{id} offer 생성 시 상품 정보 캡처
offer-service listing-service PATCH /internal/listings/{id}/status offer 수락 시 상품 → reserved
offer-service user-service GET /internal/users/{id} offer 생성 시 buyer 닉네임 캡처
offer-service chat-service POST /internal/chat/rooms offer 생성 시 자동 채팅방 생성 (fire-and-forget)
chat-service listing-service GET /internal/listings/{id} 채팅방 생성 시 상품 정보 조회
chat-service user-service GET /internal/users/{id} 채팅방 생성 시 닉네임 조회
user-service listing-service PATCH /internal/listings/seller-nickname 닉네임 변경 동기화
admin-service offer-service GET /internal/offers/stats,today,activity,trend 대시보드 집계

화면 미리보기

홈 · 상품 그리드 & 통합 검색 상품 목록 · 카테고리 필터 / 정렬
홈 상품 목록
상품 상세 · 거래 제안 실시간 채팅 · WebSocket + Redis pub/sub
상품 상세 실시간 채팅
거래함 · 보낸/받은 제안 (예약중 처리) 관리자 대시보드 · KPI & 7일 추이 차트
거래함 관리자 대시보드

분산 추적 (OpenTelemetry + Jaeger)

거래 제안 생성(POST /api/offers) 요청 하나가 offer-service → listing · user · chat 세 서비스로 fan-out되는 과정을 OpenTelemetry로 추적한 화면입니다. 단일 트랜잭션이 4개 서비스 · 31개 span에 걸쳐 실행되며, 각 서비스 간 내부 HTTP 호출과 DB / Redis 쿼리까지 하나의 타임라인에서 확인할 수 있습니다.

Jaeger 분산 추적 — POST /api/offers (4개 서비스 fan-out)


목차

  1. 핵심 특징
  2. 서비스 아키텍처
  3. 화면 미리보기
  4. 기술 스택
  5. 디렉토리 구조
  6. 서비스별 API 엔드포인트
  7. DB 스키마 (DB per Service)
  8. 환경 변수
  9. 로컬 실행 가이드
  10. 주요 설계 결정사항
  11. Redis 키 패턴
  12. 프론트엔드 페이지 구성

기술 스택

Backend

항목 기술 버전
런타임 Python 3.12
웹 프레임워크 FastAPI + uvicorn 0.115 / 0.30
DB 클라이언트 asyncpg (비동기 PostgreSQL) 0.29
캐시 / 세션 redis-py (asyncio) 5.x
인증 python-jose[cryptography] (HS256 JWT) 3.3
비밀번호 해싱 passlib[bcrypt] + bcrypt 1.7.4 / 4.0.1
유효성 검사 Pydantic v2 2.x
설정 관리 pydantic-settings 2.4
Rate Limiting slowapi 0.1.9
이미지 스토리지 boto3 (S3 presigned URL) 1.35
서비스 간 통신 httpx (비동기 HTTP 클라이언트) 0.27

Frontend

항목 기술 버전
프레임워크 Next.js (App Router) 14.x
언어 TypeScript 5.x
스타일링 Tailwind CSS 3.x
서버 상태 TanStack Query (React Query) 5.x
클라이언트 상태 Zustand 4.x
HTTP 클라이언트 Axios 1.x
폼 관리 React Hook Form + @hookform/resolvers 7.x
유효성 검사 Zod 3.x
차트 Recharts 2.x
아이콘 lucide-react —

인프라

항목 기술
컨테이너 Docker + Docker Compose
API Gateway Nginx 1.25 (WebSocket 업그레이드 지원)
데이터베이스 PostgreSQL 16 × 4 인스턴스 (DB per Service)
캐시 / Pub-Sub Redis 7
오브젝트 스토리지 AWS S3 (ap-northeast-2)
CDN AWS CloudFront
분산 추적 OpenTelemetry SDK + Jaeger 1.60

디렉토리 구조

reboot-market/
├── docker-compose.yml          # 전체 서비스 오케스트레이션
├── .env.example                # 환경 변수 템플릿
├── .env                        # 실제 환경 변수 (git 제외)
│
├── docker/                     # Docker Compose 로컬 환경 설정
│   ├── nginx/
│   │   └── nginx.conf          # API Gateway 라우팅 설정 (WebSocket 프록시 포함)
│   ├── postgres/
│   │   ├── init-user.sql       # users 테이블 스키마
│   │   ├── init-listing.sql    # categories, listings, listing_images 스키마
│   │   ├── init-offer.sql      # offers 테이블 스키마 (비정규화 컬럼 포함)
│   │   └── init-chat.sql       # chat_rooms, messages 스키마
│   └── redis/
│       └── redis.conf          # Redis 설정 (maxmemory, eviction policy 등)
│
├── services/
│   ├── user-service/           # 회원가입·로그인·JWT 발급·프로필 (포트 3001)
│   │   └── app/
│   │       ├── main.py
│   │       ├── config.py       # LISTING_SERVICE_URL 포함
│   │       ├── database.py     # postgres-user 커넥션 풀
│   │       ├── telemetry.py    # OTel SDK 초기화 (FastAPI/httpx/asyncpg/redis)
│   │       ├── routers/
│   │       │   ├── auth.py
│   │       │   ├── users.py
│   │       │   └── internal.py # GET /internal/users/{id}
│   │       └── services/
│   │           ├── auth.py     # JWT 페이로드에 nickname 포함
│   │           └── users.py    # 닉네임 변경 시 listing-service 비동기 PATCH
│   │
│   ├── listing-service/        # 상품 CRUD + S3 presigned URL (포트 3002)
│   │   └── app/
│   │       ├── telemetry.py    # OTel SDK 초기화 (FastAPI/httpx/asyncpg/redis)
│   │       ├── routers/
│   │       │   ├── listings.py
│   │       │   └── internal.py # GET /internal/listings/{id}
│   │       │                   # PATCH /internal/listings/{id}/status
│   │       │                   # PATCH /internal/listings/seller-nickname
│   │       └── services/
│   │           ├── listings.py # seller_nickname 비정규화, active+reserved 목록 반환
│   │           └── storage.py  # S3 presigned URL 생성
│   │
│   ├── offer-service/          # 거래 제안·수락·거절·취소 (포트 3003)
│   │   └── app/
│   │       ├── config.py       # LISTING_SERVICE_URL, USER_SERVICE_URL, CHAT_SERVICE_URL
│   │       ├── telemetry.py    # OTel SDK 초기화 (FastAPI/httpx/asyncpg/redis)
│   │       ├── routers/
│   │       │   ├── offers.py
│   │       │   └── internal.py # GET /internal/offers/stats,today,activity,trend
│   │       └── services/
│   │           └── offers.py   # 제안 생성 시 listing+user HTTP 조회 (비정규화)
│   │                           # 중복 제안 시 기존 pending 제안 가격 업데이트
│   │                           # accept: Phase1(로컬 트랜잭션) + Phase2(HTTP → listing)
│   │                           # 제안 생성 시 chat-service로 자동 채팅방 생성 (fire-and-forget)
│   │
│   ├── search-service/         # FTS 검색·인기검색어·카테고리 트리 (포트 3004)
│   │   └── app/
│   │       ├── telemetry.py    # OTel SDK 초기화 (FastAPI/asyncpg/redis)
│   │       ├── routers/
│   │       │   └── search.py   # 검색 API 엔드포인트
│   │       └── services/
│   │           └── search.py   # postgres-listing 직접 접근 (read-only)
│   │
│   ├── chat-service/           # 실시간 채팅 (포트 3006)
│   │   └── app/
│   │       ├── config.py       # LISTING_SERVICE_URL, USER_SERVICE_URL
│   │       ├── database.py     # postgres-chat 커넥션 풀
│   │       ├── telemetry.py    # OTel SDK 초기화
│   │       ├── routers/
│   │       │   ├── chats.py    # REST API (방 생성/목록/메시지/읽음)
│   │       │   ├── internal.py # POST /internal/chat/rooms (offer-service에서 호출)
│   │       │   └── ws.py       # WebSocket /ws/chat/{roomId} (JWT 쿼리 파라미터 인증)
│   │       └── services/
│   │           └── chat.py     # Redis pub/sub 실시간 메시징, 안읽은 수 카운터
│   │
│   └── admin-service/          # 관리자 대시보드·회원·상품 관리 (포트 3005)
│       └── app/
│           ├── database.py     # pool_user + pool_listing 듀얼 풀
│           ├── config.py       # DATABASE_URL_USER, DATABASE_URL_LISTING, OFFER_SERVICE_URL
│           ├── telemetry.py    # OTel SDK 초기화 (FastAPI/httpx/asyncpg, redis 제외)
│           ├── routers/
│           │   └── admin.py    # 관리자 전용 API 엔드포인트
│           └── services/
│               └── admin.py    # asyncio.gather로 두 DB + offer HTTP 병렬 조회
│
├── helm/reboot-market/         # Helm Chart (→ docs/infra.md 참고)
│   ├── Chart.yaml              # 차트 메타데이터
│   ├── values.yaml             # 서비스별 설정 · 글로벌 기본값
│   └── templates/              # K8s 리소스 템플릿
│       ├── deployment.yaml     # 7개 서비스 (range 기반)
│       ├── service.yaml        # ClusterIP × 7
│       ├── hpa.yaml            # HPA (CPU 70%, min2/max5)
│       ├── pdb.yaml            # PDB (minAvailable: 1)
│       ├── ingress-main.yaml   # ALB 1 — 메인 서비스 (경로 기반)
│       └── ingress-ops.yaml    # ALB 2 — 운영도구 (호스트 기반, IP 제한)
│
├── terraform/                  # AWS IaC (→ docs/infra.md 참고)
│   ├── environments/prod/      # 프로덕션 환경 설정
│   └── modules/                # VPC, EKS, RDS, ElastiCache, ECR, IAM, Bastion
│
├── .github/workflows/          # CI/CD (→ docs/cicd.md 참고)
│   ├── ci.yml                  # PR 검증 (lint/test/docker build, 변경 서비스만)
│   ├── cd.yml                  # main 머지 → ECR push → ArgoCD 배포
│   └── infra-check.yml         # Terraform validate + Helm lint
│
├── docs/
│   ├── images/                 # README 다이어그램 · 스크린샷
│   ├── infra.md                # 운영 아키텍처 설계 문서 (AWS EKS)
│   ├── eks-deploy.md           # EKS 배포 런북 (Terraform → Helm 0~11단계)
│   └── cicd.md                 # 브랜치 전략 + 파이프라인 문서
│
├── scripts/                    # 유틸리티 스크립트
│   └── test.sh                 # 통합 테스트 스크립트
│
└── frontend/                   # Next.js 14 App Router (포트 3000)
    └── src/
        ├── app/
        │   ├── page.tsx        # 홈 (최신상품 그리드 + 검색)
        │   ├── listings/
        │   │   ├── page.tsx    # 상품 목록 + 필터 (active·reserved 모두 표시)
        │   │   ├── new/
        │   │   └── [id]/       # 상품 상세 + 거래 제안 + 채팅
        │   ├── offers/         # 내 제안 / 받은 제안 (20초 폴링 자동 갱신)
        │   ├── chat/
        │   │   ├── page.tsx    # 채팅 목록 (안읽은 수 뱃지)
        │   │   └── [roomId]/   # 채팅방 (실시간 메시지, 읽음 표시)
        │   └── admin/
        ├── components/
        │   ├── layout/
        │   │   ├── Navbar.tsx  # 채팅 뱃지 + 거래함 뱃지 (pending 제안 수, 20초 폴링)
        │   │   └── Footer.tsx  # 사이트 푸터
        │   ├── listings/       # ListingCard, ListingForm, ListingGrid
        │   ├── search/         # SearchBar, CategoryFilter, RecentSearchDropdown
        │   └── offers/
        │       └── OfferForm.tsx
        ├── hooks/
        │   ├── useOffers.ts    # refetchInterval: 20s
        │   └── useChat.ts     # 채팅 API hooks (rooms, messages, unread)
        └── lib/
            └── api.ts          # Axios 클라이언트 (서비스별 baseURL + 토큰 인터셉터)

서비스별 API 엔드포인트

user-service — 포트 3001

Method Path 인증 설명
GET /health — 헬스체크
POST /api/auth/register — 회원가입
POST /api/auth/login — 로그인 (access + refresh 토큰 발급)
POST /api/auth/refresh — access 토큰 갱신
POST /api/auth/logout ✓ 로그아웃 (Redis refresh 토큰 무효화)
GET /api/users/me ✓ 내 프로필 조회
PATCH /api/users/me ✓ 내 프로필 수정 (닉네임 변경 시 listing-service 비동기 동기화)
GET /api/users/{userId} — 공개 프로필 조회
GET /internal/users/{userId} 내부 전용 offer/chat 생성 시 닉네임 조회

JWT 전략:

  • Access Token: python-jose HS256 서명, TTL 15m, Authorization: Bearer <token> 헤더
  • Refresh Token: TTL 7d, Redis refresh:{user_id}에 저장
  • JWT 페이로드: sub, role, email, nickname (listing-service에서 seller_nickname 사용)
  • 각 서비스가 JWT_ACCESS_SECRET으로 독립 검증

listing-service — 포트 3002

Method Path 인증 설명
GET /health — 헬스체크
GET /api/listings — 목록 조회 (active + reserved 포함, 카테고리·가격 필터, 페이지네이션)
POST /api/listings ✓ 상품 등록 (JWT의 nickname → seller_nickname 비정규화)
GET /api/listings/me ✓ 내 상품 목록
GET /api/listings/{id} — 상세 조회 (조회수 자동 증가, Redis 캐시 120s)
PATCH /api/listings/{id} ✓ 소유자 상품 수정
DELETE /api/listings/{id} ✓ 소유자 상품 soft delete (status='deleted')
POST /api/listings/presigned-url ✓ S3 presigned PUT URL 발급
POST /api/listings/{id}/images ✓ 소유자 이미지 메타데이터 저장
GET /internal/listings/{id} 내부 전용 offer/chat 생성 시 listing 정보 조회
PATCH /internal/listings/{id}/status 내부 전용 offer accept 시 listing 상태 변경 (멱등성 보장)
PATCH /internal/listings/seller-nickname 내부 전용 닉네임 변경 시 비정규화 동기화

offer-service — 포트 3003

Method Path 인증 설명
GET /health — 헬스체크
POST /api/offers ✓ 거래 제안 생성 (기존 pending 제안 시 가격 업데이트)
GET /api/offers ✓ 내가 보낸 제안 목록
GET /api/offers/received ✓ 내가 받은 제안 목록 (판매자)
PATCH /api/offers/{id}/accept ✓ 판매자 제안 수락
PATCH /api/offers/{id}/reject ✓ 판매자 제안 거절
PATCH /api/offers/{id}/cancel ✓ 구매자 제안 취소
GET /internal/offers/stats 내부 전용 admin 통계 집계
GET /internal/offers/today 내부 전용 admin 오늘 현황
GET /internal/offers/activity 내부 전용 admin 활동 피드
GET /internal/offers/trend 내부 전용 admin 7일 트렌드

제안 생성 흐름 (비정규화 + 채팅방 자동 생성):

POST /api/offers
  ├─ GET listing-service /internal/listings/{id}  → listing 정보 캡처
  ├─ GET user-service /internal/users/{buyer_id}  → buyer 닉네임 캡처
  ├─ 기존 pending 제안 존재? → UPDATE offered_price, message (가격 변경)
  ├─ 신규? → INSERT offers (비정규화 컬럼 포함)
  └─ POST chat-service /internal/chat/rooms       → 채팅방 자동 생성 (fire-and-forget)

수락 2-Phase 처리:

Phase 1 (offer-db 로컬 트랜잭션):
  BEGIN
    SELECT offer FOR UPDATE          → 동시 수락 방지
    UPDATE offers SET status='accepted'
    UPDATE offers SET status='rejected' WHERE listing_id=$1 AND status='pending'
  COMMIT

Phase 2 (HTTP):
  PATCH listing-service /internal/listings/{id}/status {"status":"reserved"}

실패 시 보상 트랜잭션:
  UPDATE offers SET status='pending' WHERE offer_id=$1   ← rollback

search-service — 포트 3004

Method Path 인증 설명
GET /health — 헬스체크
GET /api/search — 전문 검색 (PostgreSQL FTS + Redis 캐시 60s)
GET /api/search/popular — 인기 검색어 Top 10
GET /api/search/categories — 카테고리 전체 조회

쿼리 파라미터: q, category_id, min_price, max_price, sort (latest/price_asc/price_desc/popular), page, limit

검색 구현:

  • plainto_tsquery('simple', ?) + GIN 인덱스 (idx_listings_fts)
  • active + reserved 상태 모두 검색 결과에 포함
  • 쿼리 파라미터 MD5 해시 → Redis 캐시 키
  • 검색 시 ZINCRBY popular:searches 1 {term} (TTL 1시간)

chat-service — 포트 3006

Method Path 인증 설명
GET /health — 헬스체크
POST /api/chat/rooms ✓ 채팅방 생성 또는 기존 반환
GET /api/chat/rooms ✓ 내 채팅방 목록 (안읽은 수 포함)
GET /api/chat/rooms/{roomId} ✓ 채팅방 상세
GET /api/chat/rooms/{roomId}/messages ✓ 메시지 목록 (페이지네이션, DESC)
POST /api/chat/rooms/{roomId}/messages ✓ 메시지 전송 (Redis pub/sub 브로드캐스트)
PATCH /api/chat/rooms/{roomId}/read ✓ 읽음 처리 (Redis 카운터 감소)
GET /api/chat/unread ✓ 전체 안읽은 메시지 수
WS /ws/chat/{roomId}?token=... JWT 쿼리 WebSocket 실시간 메시지 수신
POST /internal/chat/rooms 내부 전용 offer-service에서 자동 채팅방 생성

실시간 메시징 흐름:

① 메시지 전송: POST /api/chat/rooms/{roomId}/messages
  ├─ INSERT messages
  ├─ UPDATE chat_rooms.last_message
  ├─ Redis PUBLISH chat:{roomId} (메시지 JSON)
  └─ Redis INCR chat:unread:{recipient_id}

② WebSocket 수신: /ws/chat/{roomId}?token=JWT
  ├─ JWT 검증 (쿼리 파라미터)
  ├─ Redis SUBSCRIBE chat:{roomId}
  └─ 메시지 수신 시 클라이언트에 JSON 전송

③ 읽음 처리: PATCH /api/chat/rooms/{roomId}/read
  ├─ UPDATE messages SET read_at = NOW()
  ├─ Redis PUBLISH chat:{roomId} (read 이벤트)
  └─ Redis DECRBY chat:unread:{user_id}

admin-service — 포트 3005

모든 엔드포인트는 role='admin' JWT 필수

Method Path 설명
GET /health 헬스체크
GET /api/admin/stats 대시보드 지표 (전체 사용자·상품·제안·거래액)
GET /api/admin/stats/trend 최근 7일 일별 트렌드
GET /api/admin/monitor/today 오늘 현황
GET /api/admin/monitor/activity 최근 25건 활동 피드
GET /api/admin/users 전체 회원 목록
PATCH /api/admin/users/{id}/role 역할 변경 (user ↔ admin)
GET /api/admin/listings 전체 상품 목록 (상태 필터)
PATCH /api/admin/listings/{id}/status 상품 상태 강제 변경
POST /api/admin/categories 카테고리 생성
PATCH /api/admin/categories/{id} 카테고리 수정 (이름, 아이콘, 색상)
DELETE /api/admin/categories/{id} 카테고리 삭제

admin-service 데이터 수집 방식:

  • pool_user (postgres-user) + pool_listing (postgres-listing) 듀얼 커넥션 풀
  • offer 데이터는 offer-service 내부 엔드포인트 HTTP 호출
  • asyncio.gather()로 3개 소스 병렬 조회

DB 스키마 (DB per Service)

DB 토폴로지

PostgreSQL 컨테이너 호스트 포트 DB 이름 사용 서비스
postgres-user 5433 reboot_user user-service
postgres-listing 5434 reboot_listing listing-service, search-service (read), admin-service (dual pool)
postgres-offer 5435 reboot_offer offer-service
postgres-chat 5436 reboot_chat chat-service

테이블 구조

-- [postgres-user] reboot_user
users (
  user_id UUID PK, email UNIQUE, password_hash,
  nickname UNIQUE, role CHECK('user','admin'), created_at
)

-- [postgres-listing] reboot_listing
categories (category_id SERIAL PK, name, parent_id → self FK, icon, color)

listings (
  listing_id UUID PK, seller_id UUID,
  seller_nickname VARCHAR(100),     ← 비정규화 (user-db FK 없음)
  category_id → categories, title, description, price,
  status CHECK('active','reserved','sold','deleted'),
  view_count, created_at, updated_at
)

listing_images (
  image_id SERIAL PK, listing_id → listings CASCADE,
  object_key VARCHAR(512), sort_order, is_primary
)

-- [postgres-offer] reboot_offer
offers (
  offer_id UUID PK, listing_id UUID, buyer_id UUID,
  offered_price NUMERIC, status CHECK('pending','accepted','rejected','cancelled'),
  message TEXT,
  -- 비정규화 컬럼 (생성 시점 캡처, cross-domain JOIN 불필요)
  listing_title VARCHAR(200),
  listing_price NUMERIC,
  listing_seller_id UUID,           ← 소유권 확인용
  buyer_nickname VARCHAR(100),
  primary_image_key VARCHAR(512),
  created_at, updated_at
)

-- [postgres-chat] reboot_chat
chat_rooms (
  room_id UUID PK, listing_id UUID, buyer_id UUID, seller_id UUID,
  -- 비정규화 컬럼 (생성 시점 캡처)
  listing_title VARCHAR(200),
  listing_price NUMERIC,
  primary_image_key VARCHAR(512),
  buyer_nickname VARCHAR(100),
  seller_nickname VARCHAR(100),
  last_message TEXT,
  last_message_at TIMESTAMPTZ,
  created_at
)
UNIQUE INDEX (listing_id, buyer_id)  ← 동일 상품·구매자 1개 방

messages (
  message_id UUID PK, room_id → chat_rooms CASCADE,
  sender_id UUID, content TEXT,
  read_at TIMESTAMPTZ,              ← NULL이면 안읽음
  created_at
)

비정규화 전략

테이블 비정규화 컬럼 동기화 시점
listings.seller_nickname users.nickname 닉네임 변경 시 user-service → listing-service HTTP PATCH (fire-and-forget)
offers.listing_title/price/seller_id/image listings 정보 offer 생성 시 listing-service HTTP 조회 후 스냅샷
offers.buyer_nickname users.nickname offer 생성 시 user-service HTTP 조회 후 스냅샷
chat_rooms.listing_title/price/image listings 정보 채팅방 생성 시 listing-service HTTP 조회 후 스냅샷
chat_rooms.buyer/seller_nickname users.nickname 채팅방 생성 시 user-service HTTP 조회 후 스냅샷

인덱스

  • idx_listings_fts — GIN 인덱스, FTS 검색
  • idx_listings_status, idx_listings_created_at, idx_listings_price — 필터·정렬
  • idx_offers_listing_buyer_active — Partial UNIQUE WHERE status = 'pending' (취소·거절·수락 후 재제안 허용)
  • idx_chat_rooms_listing_buyer — UNIQUE (listing_id, buyer_id), 동일 상품·구매자 중복 방 방지
  • updated_at 트리거 — listings, offers 자동 갱신

환경 변수

.env.example을 복사하여 .env로 사용합니다.

cp .env.example .env
변수 설명 예시
POSTGRES_USER PostgreSQL 사용자 reboot
POSTGRES_PASSWORD PostgreSQL 비밀번호 reboot1234
JWT_ACCESS_SECRET Access Token 서명 키 (32자 이상) (변경 필수)
JWT_REFRESH_SECRET Refresh Token 서명 키 (32자 이상) (변경 필수)
JWT_ACCESS_EXPIRES_IN Access Token 만료 15m
JWT_REFRESH_EXPIRES_IN Refresh Token 만료 7d
BCRYPT_ROUNDS bcrypt 해싱 라운드 10
AWS_ACCESS_KEY_ID AWS IAM Access Key (필수)
AWS_SECRET_ACCESS_KEY AWS IAM Secret Key (필수)
AWS_REGION S3 리전 ap-northeast-2
S3_BUCKET_NAME S3 버킷 이름 (필수)
CLOUDFRONT_DOMAIN CloudFront 도메인 (필수)
SEARCH_CACHE_TTL 검색 결과 캐시 TTL(초) 60
POPULAR_TERMS_TTL 인기 검색어 TTL(초) 3600
OTEL_EXPORTER_OTLP_ENDPOINT Jaeger OTLP gRPC 엔드포인트 (비어있으면 비활성화) http://jaeger:4317

로컬 실행 가이드

로컬 개발/검증은 Docker Compose, 프로덕션 배포는 AWS EKS (Helm + ArgoCD) 로 구성된 2-tier 환경입니다. 운영 배포는 infra.md · cicd.md 를 참고하세요.

사전 요구사항

  • Docker Desktop 4.x 이상
  • Docker Compose V2 (docker compose 명령)
  • AWS 계정 (S3 버킷 + CloudFront 배포) — 이미지 업로드 기능 사용 시

실행

# 1. 레포지토리 클론
git clone <repo-url>
cd reboot-market

# 2. 환경 변수 설정
cp .env.example .env
# .env 파일에서 AWS_*, JWT_*_SECRET 값을 채워넣으세요

# 3. 전체 빌드 및 기동
docker compose up -d --build

# 4. 헬스체크 확인
curl http://localhost:3333/health/user
curl http://localhost:3333/health/listing
curl http://localhost:3333/health/offer
curl http://localhost:3333/health/search
curl http://localhost:3333/health/chat
curl http://localhost:3333/health/admin

접속 URL

서비스 URL
서비스 전체 진입점 (nginx) http://localhost:3333
프론트엔드 (직접) http://localhost:3000
user-service (직접) http://localhost:3001
listing-service (직접) http://localhost:3002
offer-service (직접) http://localhost:3003
search-service (직접) http://localhost:3004
admin-service (직접) http://localhost:3005
chat-service (직접) http://localhost:3006
Jaeger UI (분산 추적) http://localhost:16686
postgres-user localhost:5433
postgres-listing localhost:5434
postgres-offer localhost:5435
postgres-chat localhost:5436
Redis localhost:6379

관리자 계정 생성

초기 DB에는 관리자 계정이 없습니다. 일반 회원가입 후 DB에서 직접 역할을 변경하세요.

# 회원가입 후 관리자로 승격
docker compose exec postgres-user psql -U reboot -d reboot_user \
  -c "UPDATE users SET role='admin' WHERE email='your@email.com';"

종료 및 데이터 초기화

# 컨테이너 중지 (데이터 유지)
docker compose down

# 컨테이너 + 볼륨 전체 삭제 (DB 초기화)
docker compose down -v

주요 설계 결정사항

항목 결정 이유
DB 분리 DB per Service (4개 PostgreSQL 인스턴스) MSA 원칙, 서비스 독립 배포·스케일링
Cross-domain JOIN 제거 비정규화 (seller_nickname, offer/chat 스냅샷 컬럼) 서비스 간 DB 공유 불가 → 생성 시점 스냅샷 저장
서비스 간 통신 httpx 비동기 HTTP (내부 엔드포인트) Kafka 없이 단순화, 로컬 환경 적합
분산 트랜잭션 2-Phase + 보상 트랜잭션 (Saga 패턴 간소화) Kafka/사가 없이 일관성 확보, ~200ms 창 허용
JWT 검증 각 서비스 독립 검증 JWT_ACCESS_SECRET 공유로 서비스 간 통신 불필요
이미지 업로드 presigned URL → 클라이언트 직접 S3 PUT 백엔드 대역폭 절감
전문 검색 PostgreSQL FTS (GIN 인덱스) 외부 검색 엔진 없이 구현, simple 사전
인기 검색어 Redis Sorted Set (ZINCRBY) 원자적 증가, ZREVRANGE로 Top N 즉시 조회
상품 목록 active + reserved 동시 노출 예약중 상품도 탐색 가능, 판매완료만 제외
Offer 중복 처리 기존 pending 제안 가격/메시지 UPDATE 재제안 = 가격 변경, 당근마켓 방식 UX
Offer 중복 방지 Partial UNIQUE WHERE status='pending' 수락·거절·취소 후 재제안 허용
실시간 채팅 WebSocket + Redis pub/sub 서비스 수평 확장 시에도 메시지 브로드캐스트 가능
채팅방 자동 생성 offer 생성 시 fire-and-forget HTTP 채팅 실패해도 제안은 유지, 느슨한 결합
WebSocket 인증 JWT 쿼리 파라미터 WebSocket은 커스텀 헤더 불가 → 쿼리로 전달
안읽은 메시지 수 Redis 카운터 + DB fallback 실시간 카운터 → 빠른 조회, DB 정합성 보장
실시간 알림 React Query refetchInterval: 20s 거래함·채팅 목록 자동 갱신
admin 집계 asyncio.gather() (dual pool + HTTP) Kafka 없이 병렬 조회로 응답 시간 최소화
Soft Delete status='deleted' 거래 이력·통계 보존

Redis 키 패턴

키 타입 TTL 설명
refresh:{user_id} String 7일 Refresh Token (로그아웃 시 DEL)
listing:{listing_id} String (JSON) 120초 상품 상세 캐시 (수정/삭제 시 DEL)
search:{md5hash} String (JSON) 60초 검색 결과 캐시 (쿼리 파라미터 MD5 해시)
popular:searches Sorted Set 1시간 인기 검색어 (ZINCRBY, ZREVRANGE 0 9)
offers:buyer:{id}:* String (JSON) 30초 내가 보낸 제안 목록 캐시
offers:seller:{id}:* String (JSON) 30초 내가 받은 제안 목록 캐시
chat:unread:{user_id} String (int) 300초 안읽은 메시지 총 수 (INCR/DECRBY)
chat:{room_id} Pub/Sub 채널 — 실시간 메시지 브로드캐스트

프론트엔드 페이지 구성

경로 설명 인증
/ 홈 — 최신 상품 그리드 + 검색창 —
/login 로그인 폼 —
/register 회원가입 폼 —
/listings 상품 목록 + 카테고리 필터 + 정렬 (예약중 배지 포함) —
/listings/new 상품 등록 + 이미지 업로드 (S3 presigned) ✓
/listings/[id] 상품 상세 + 채팅하기 + 거래 제안 (기존 제안 시 가격 변경 UI) —
/listings/[id]/edit 상품 수정 폼 ✓ 소유자
/offers 내 제안 / 받은 제안 탭 (20초 폴링 자동 갱신) ✓
/chat 채팅 목록 (상품 이미지, 상대 닉네임, 마지막 메시지, 안읽은 수 뱃지) ✓
/chat/[roomId] 채팅방 (메시지 버블, 시간 그룹핑, 읽음 표시, 자동 스크롤) ✓
/profile/[userId] 공개 프로필 + 판매 상품 목록 —
/admin 관리자 대시보드 (6개 서비스 헬스 모니터링 포함) ✓ admin

Navbar:

  • 채팅 — 안읽은 메시지 수 뱃지 (20초 폴링)
  • 거래함 — pending 받은 제안 수 뱃지 (20초 폴링)
  • React Query 쿼리 키 공유 → 페이지와 Navbar 간 중복 요청 없음

관리자 대시보드 탭:

  • 대시보드 — 8개 KPI 카드 + 7일 추이 차트 (LineChart / BarChart)
  • 회원관리 — 전체 회원 목록 + 역할 변경
  • 상품관리 — 전체 상품 목록 + 상태 필터 + 강제 상태 변경
  • 카테고리관리 — 카테고리 생성·수정(아이콘/색상)·삭제
  • 모니터링 — 오늘 현황 / 6개 서비스 헬스(응답시간) / 인기 검색어 TOP 10 / 활동 피드 / 이상 감지

라이선스

이 프로젝트는 학습 목적으로 작성되었습니다.

About

중고거래 플랫폼 Reboot Market

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages