JK
프로젝트
JK
>로딩 중 // 로딩 중
뷰 초기화 중
제작 및 디자인:JK
JK
>로딩 중 // PROJECT DETAILS
PARSING ARCHITECTURE, DIAGRAMS & API ENDPOINTS
프로젝트 목록으로 돌아가기
GitHub에서 보기
주식 리워드 보상 시스템
완료gopostgresgingolang-migratevalidatorlogrusdocker

주식 리워드 보상 시스템

복식부기 회계를 적용하여 거래의 투명성과 정확한 INR 평가를 보장하는 주식 보상 관리 백엔드.

문제 및 해결책

주식 보상 관리는 엄격한 거래 무결성과 감사 추적성이 필수적이며, 외부 주가 API 장애 시에도 서비스가 중단되지 않는 회복탄력성이 요구됩니다.

기능

  • 불변 복식부기 거래 원장 시스템
  • 실시간 주가 조회 및 자동 폴백 캐싱
  • 주식 베스팅(권리 확정) 및 수량 조정 자동화
  • 규정 준수를 위한 완전한 감사 추적 기록
  • PostgreSQL 트랜잭션 무결성 보장

기술 스택

기술버전목적선택 이유
Go1.25.1프로그래밍 언어강력한 표준 라이브러리, 동시성 기본 요소, 빠른 컴파일
Gin1.11.0HTTP 프레임워크, 라우팅, 미들웨어고성능, 미들웨어 지원, 활발한 커뮤니티
PostgreSQL17관계형 데이터베이스고급 SQL 기능 (뷰, LATERAL, 생성 컬럼, ENUM)
golang-migratev4.19.0데이터베이스 마이그레이션간단한 파일 기반 마이그레이션 러너
logrus1.9.3구조화 로깅구조화된 필드와 레벨별 로깅
go-sqlmockv1.5.2테스트용 데이터베이스 모킹실제 DB 없이 SQL을 시뮬레이션
lib/pqv1.10.9PostgreSQL 드라이버검증된 안정성, 네이티브 PostgreSQL 지원
Docker—컨테이너화재현 가능한 빌드, 격리된 환경

아키텍처

Docker Compose

Gin HTTP Server

:8080

Handlers

Reward, Adjustment, Today, Historical, Stats, Portfolio

PriceService Worker Pool

PriceCache

in-memory RWMutex

RandomPriceFetcher

PostgreSQL 17

:5432

7 Tables

3 Views

today_rewards, historical_rewards, user_portfolio

2 ENUMs

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.

핵심 구성 요소

  • 요청 추적을 위한 RequestID 미들웨어를 갖춘 Gin HTTP 서버
  • SAVEPOINT 기반 멱등성 패턴을 사용하는 리워드 핸들러
  • SELECT FOR UPDATE 행 수준 잠금을 사용하는 조정 핸들러
  • PostgreSQL 뷰를 조회하는 읽기 핸들러 (today, historical, portfolio, stats)
  • 5개 고루틴 워커 풀과 DB용 3-슬롯 세마포어를 갖춘 백그라운드 PriceService

시스템 복원력

  • 읽기 중심 워크로드를 위한 RWMutex 기반 인메모리 PriceCache
  • CAS 기반 동시성 제어로 연쇄적인 업데이트 지연 방지
  • 가격 업데이터 워커의 패닉 복구로 프로세스 크래시 방지
  • 10초 드레인 타임아웃을 적용한 우아한 HTTP 종료
  • 모든 쓰기 작업에 지연 롤백을 적용한 트랜잭션 래핑

프로젝트 구조

cmd
stocky-api
main.go진입점: DB 설정, 마이그레이션, 백그라운드 작업, HTTP 서버, 우아한 종료
internal
config
database
migrations
handlers
stocky
jobs
price_updater.go워커 풀, 캐시, 재시도, 시뮬레이션된 fetcher를 갖춘 백그라운드 PriceService
storage
models
utils

API 엔드포인트

Stocky API (포트 8080)

메서드경로목적
GET/health상태 확인 — {status: OK} 반환
POST/api/v1/reward리워드 생성 — 멱등, 트랜잭션, 수수료 계산
POST/api/v1/adjustments/:id조정 적용 — 행 잠금, 유형별 원장 항목
GET/api/v1/today-stocks/:userIdtoday_rewards 뷰에서 오늘의 리워드 조회
GET/api/v1/historical-inr/:userId과거 INR 평가액 — 페이지네이션, 이벤트 승수 적용
GET/api/v1/stats/:userId통계 요약 — 오늘의 리워드 + 총 포트폴리오 가치
GET/api/v1/portfolio/:userId포트폴리오 상세 — 페이지네이션, 상장 폐지 필터

데이터베이스 설계

users

4 cols
idPK
serial
username
varchar
email
varchar
created_at
timestamp

rewards

6 cols
idPK
serial
user_id
int
stock_symbol
varchar
quantity
numeric(18,6)
idempotency_key
varchar
reward_date
date

ledger

6 cols
idPK
serial
reward_id
int
entry_type
ledger_entry_type
stock_symbol
varchar
quantity
numeric(18,6)
amount
numeric(18,4)

adjustments

6 cols
idPK
serial
reward_id
int
adjustment_type
varchar
delta_quantity
numeric(18,6)
delta_amount
numeric(18,4)
reason
varchar

stock_prices

4 cols
idPK
serial
stock_symbol
varchar
price
numeric(18,4)
updated_at
timestamp

stock_price_history

4 cols
idPK
serial
stock_symbol
varchar
price
numeric(18,4)
date
date

stock_events

6 cols
idPK
serial
stock_symbol
varchar
event_type
stock_event_type
ratio_num
numeric(18,6)
ratio_den
numeric(18,6)
new_symbol
varchar
Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.

users

컬럼유형비고
idPKSERIAL자동 생성
usernameVARCHAR(50)NOT NULL, UNIQUE
emailVARCHAR(100)NOT NULL, UNIQUE
created_atTIMESTAMP기본값: NOW()

사용자 계정. 테스트 사용자 1명이 시드됩니다.

rewards

컬럼유형비고
idPKSERIAL자동 생성
user_idINTFK → users(id) ON DELETE CASCADE
stock_symbolVARCHAR(20)NOT NULL
quantityNUMERIC(18,6)NOT NULL, 음수 = 취소
idempotency_keyVARCHAR(64)UNIQUE, NULL 허용
reward_dateDATEGENERATED ALWAYS AS (created_at::DATE) STORED

핵심 리워드 이벤트. (user_id, stock_symbol, reward_date)에 고유 제약이 있으며 대소문자 무시 변형이 있음.

ledger

컬럼유형비고
idPKSERIAL자동 생성
reward_idINTFK → rewards(id) ON DELETE CASCADE
entry_typeledger_entry_typeENUM: stock_units, inr_outflow, fees, brokerage_fee, stt_fee, gst_fee
stock_symbolVARCHAR(20)NULL 허용
quantityNUMERIC(18,6)NULL 허용
amountNUMERIC(18,4)NULL 허용

복식부기 원장. 각 리워드는 2~5개의 항목을 생성함.

stock_prices

컬럼유형비고
idPKSERIAL자동 생성
stock_symbolVARCHAR(20)UNIQUE
priceNUMERIC(18,4)현재 가격
updated_atTIMESTAMPPriceService가 업데이트

종목별 최신 알려진 가격. 백그라운드 PriceService가 업데이트함.

stock_price_history

컬럼유형비고
idPKSERIAL자동 생성
stock_symbolVARCHAR(20)NOT NULL
priceNUMERIC(18,4)NOT NULL
dateDATENOT NULL, stock_symbol과 복합 UNIQUE

과거 평가용 일별 가격 스냅샷. PriceService가 업서트함.

stock_events

컬럼유형비고
idPKSERIAL자동 생성
stock_symbolVARCHAR(20)NOT NULL
event_typestock_event_typeENUM: split, bonus, merge, delist
ratio_numNUMERIC(18,6)승수 분자
ratio_denNUMERIC(18,6)승수 분모
new_symbolVARCHAR(20)합병용
effective_dateTIMESTAMPNOT NULL

기업 이벤트. ratio_num/ratio_den이 승수를 정의함 (예: 2:1 분할).

adjustments

컬럼유형비고
idPKSERIAL자동 생성
reward_idINTFK → rewards(id) ON DELETE CASCADE
adjustment_typeVARCHAR(64)reward_reversal, fee_refund, manual_correction
delta_quantityNUMERIC(18,6)NOT NULL
delta_amountNUMERIC(18,4)NOT NULL
reasonVARCHAR(255)NULL 허용

행 수준 잠금을 적용한 기존 리워드 수정.

일대다: users→rewards (CASCADE), rewards→ledger (CASCADE), rewards→adjustments (CASCADE). 암시적 상호 참조: rewards.stock_symbol ↔ stock_prices.stock_symbol (UPPER() 경유), rewards.stock_symbol ↔ stock_events.stock_symbol (이벤트 승수). rewards와 stock_prices/stock_events 사이에 공식 FK는 없으며 뷰 수준에서 강제됨.

주요 설계 결정

집계를 위한 SQL 뷰

결정: 복잡한 포트폴리오/과거 계산을 애플리케이션 코드 대신 PostgreSQL 뷰로 구현

이유: 애플리케이션 복잡성을 줄이고 PostgreSQL 쿼리 옵티마이저를 활용하며 계산의 단일 진실 공급원을 제공

이점: 모든 엔드포인트에서 일관된 계산, DB가 JOIN과 집계를 효율적으로 처리

트레이드오프: 대규모 데이터셋에서는 뷰가 느려질 수 있고, 애플리케이션 코드보다 디버깅이 어려우며, PostgreSQL에 종속됨

승수를 위한 로그 곱

결정: 누적 기업 이벤트 승수를 계산하기 위해 EXP(SUM(LN(ratio))) 사용

이유: 많은 이벤트가 합성될 때 부동소수점 오버플로/언더플로 방지

이점: 수치적으로 안정적이며 임의 개수의 이벤트 처리 가능

트레이드오프: 직접 곱셈보다 읽기 어렵고 양수 비율이 필요함

핸들러 전용 아키텍처 (서비스 계층 없음)

결정: 비즈니스 로직이 별도 서비스 계층 없이 핸들러에 직접 위치

이유: 소규모 프로젝트에 더 단순하고 유지할 파일과 추상화가 적음

이점: 보일러플레이트가 적고 요청 흐름 추적이 쉬움

트레이드오프: 로직 재사용이 어렵고 HTTP와 데이터 접근의 결합이 강하며 비즈니스 로직을 독립적으로 테스트하기 어려움

시뮬레이션된 가격 fetcher

결정: 실제 시장 데이터 API 대신 RandomPriceFetcher 사용

이유: 개발/테스트 환경이며 API 키 관리가 필요 없음

이점: 외부 의존성이 전혀 없고 거의 결정론적 동작

트레이드오프: 프로덕션 준비가 아니며 포트폴리오 가치가 의미 없고 실제 배포 시 교체해야 함

SAVEPOINT 기반 멱등성

결정: 외부 트랜잭션을 중단하지 않고 멱등성을 처리하기 위해 PostgreSQL SAVEPOINT 사용

이유: 외부 멱등성 서비스 없이 재시도 안전성 확보

이점: PostgreSQL 제약 내에서 동작하는 정교한 트랜잭션 내 멱등성

트레이드오프: 복잡한 오류 처리와 SAVEPOINT 오버헤드

인증/인가 없음

결정: 모든 엔드포인트가 인증 없이 공개 접근 가능

이유: 개발/데모 프로젝트이며 인증은 나중에 추가 예정 (예: API 게이트웨이 경유)

이점: 구현이 단순하고 테스트가 쉬움

트레이드오프: 프로덕션에 부적합하며 모든 클라이언트가 리워드를 생성하거나 조정을 적용할 수 있음

테스트

Go 표준 테스팅 + go-sqlmock v1.5.2•단위 테스트 19개

테스트 항목

  • 리워드 핸들러: NULL 멱등성, 잘못된 JSON, 0/음수 수량, 정규화, 멱등 재시도 (8개 테스트)
  • 조정 핸들러: 잘못된 리워드 ID, 잘못된 JSON, 잘못된 유형, 취소 시 원장 동작 (4개 테스트)
  • 과거 핸들러: 잘못된 userId, 잘못된 limit (2개 테스트)
  • 포트폴리오 핸들러: 잘못된 userId, 잘못된 offset, 대소문자 무시 상장 폐지 필터 (3개 테스트)
  • 오늘 핸들러: 잘못된 userId (1개 테스트)
  • 통계 핸들러: 잘못된 userId (1개 테스트)

부족한 커버리지

  • 실제 PostgreSQL에 대한 통합 테스트
  • 실제 데이터 검증을 포함한 읽기 핸들러 테스트
  • 백그라운드 가격 업데이터 테스트 (오류 경로, 패닉, 재시도)
  • 동시 조정 처리 테스트
  • 트랜잭션 롤백 시나리오 테스트
  • 페이지네이션 경계 사례 테스트

주요 과제

  • 동시 금융 거래 환경에서 레이스 컨디션의 완전한 방지
  • 외부 시세 API 장애 시 신뢰할 수 있는 폴백 체계 구축

주요 학습

  • 관계형 데이터베이스에서의 복식부기 원장 모델링
  • 금융 거래 무결성을 위한 비관적/낙관적 락 활용