JK
>로딩 중 // 로딩 중
뷰 초기화 중
복식부기 회계를 적용하여 거래의 투명성과 정확한 INR 평가를 보장하는 주식 보상 관리 백엔드.
주식 보상 관리는 엄격한 거래 무결성과 감사 추적성이 필수적이며, 외부 주가 API 장애 시에도 서비스가 중단되지 않는 회복탄력성이 요구됩니다.
| 기술 | 버전 | 목적 | 선택 이유 |
|---|---|---|---|
| Go | 1.25.1 | 프로그래밍 언어 | 강력한 표준 라이브러리, 동시성 기본 요소, 빠른 컴파일 |
| Gin | 1.11.0 | HTTP 프레임워크, 라우팅, 미들웨어 | 고성능, 미들웨어 지원, 활발한 커뮤니티 |
| PostgreSQL | 17 | 관계형 데이터베이스 | 고급 SQL 기능 (뷰, LATERAL, 생성 컬럼, ENUM) |
| golang-migrate | v4.19.0 | 데이터베이스 마이그레이션 | 간단한 파일 기반 마이그레이션 러너 |
| logrus | 1.9.3 | 구조화 로깅 | 구조화된 필드와 레벨별 로깅 |
| go-sqlmock | v1.5.2 | 테스트용 데이터베이스 모킹 | 실제 DB 없이 SQL을 시뮬레이션 |
| lib/pq | v1.10.9 | PostgreSQL 드라이버 | 검증된 안정성, 네이티브 PostgreSQL 지원 |
| Docker | — | 컨테이너화 | 재현 가능한 빌드, 격리된 환경 |
| 메서드 | 경로 | 목적 |
|---|---|---|
| GET | /health | 상태 확인 — {status: OK} 반환 |
| POST | /api/v1/reward | 리워드 생성 — 멱등, 트랜잭션, 수수료 계산 |
| POST | /api/v1/adjustments/:id | 조정 적용 — 행 잠금, 유형별 원장 항목 |
| GET | /api/v1/today-stocks/:userId | today_rewards 뷰에서 오늘의 리워드 조회 |
| GET | /api/v1/historical-inr/:userId | 과거 INR 평가액 — 페이지네이션, 이벤트 승수 적용 |
| GET | /api/v1/stats/:userId | 통계 요약 — 오늘의 리워드 + 총 포트폴리오 가치 |
| GET | /api/v1/portfolio/:userId | 포트폴리오 상세 — 페이지네이션, 상장 폐지 필터 |
| 컬럼 | 유형 | 비고 |
|---|---|---|
| idPK | SERIAL | 자동 생성 |
| username | VARCHAR(50) | NOT NULL, UNIQUE |
| VARCHAR(100) | NOT NULL, UNIQUE | |
| created_at | TIMESTAMP | 기본값: NOW() |
사용자 계정. 테스트 사용자 1명이 시드됩니다.
| 컬럼 | 유형 | 비고 |
|---|---|---|
| idPK | SERIAL | 자동 생성 |
| user_id | INT | FK → users(id) ON DELETE CASCADE |
| stock_symbol | VARCHAR(20) | NOT NULL |
| quantity | NUMERIC(18,6) | NOT NULL, 음수 = 취소 |
| idempotency_key | VARCHAR(64) | UNIQUE, NULL 허용 |
| reward_date | DATE | GENERATED ALWAYS AS (created_at::DATE) STORED |
핵심 리워드 이벤트. (user_id, stock_symbol, reward_date)에 고유 제약이 있으며 대소문자 무시 변형이 있음.
| 컬럼 | 유형 | 비고 |
|---|---|---|
| idPK | SERIAL | 자동 생성 |
| reward_id | INT | FK → rewards(id) ON DELETE CASCADE |
| entry_type | ledger_entry_type | ENUM: stock_units, inr_outflow, fees, brokerage_fee, stt_fee, gst_fee |
| stock_symbol | VARCHAR(20) | NULL 허용 |
| quantity | NUMERIC(18,6) | NULL 허용 |
| amount | NUMERIC(18,4) | NULL 허용 |
복식부기 원장. 각 리워드는 2~5개의 항목을 생성함.
| 컬럼 | 유형 | 비고 |
|---|---|---|
| idPK | SERIAL | 자동 생성 |
| stock_symbol | VARCHAR(20) | UNIQUE |
| price | NUMERIC(18,4) | 현재 가격 |
| updated_at | TIMESTAMP | PriceService가 업데이트 |
종목별 최신 알려진 가격. 백그라운드 PriceService가 업데이트함.
| 컬럼 | 유형 | 비고 |
|---|---|---|
| idPK | SERIAL | 자동 생성 |
| stock_symbol | VARCHAR(20) | NOT NULL |
| price | NUMERIC(18,4) | NOT NULL |
| date | DATE | NOT NULL, stock_symbol과 복합 UNIQUE |
과거 평가용 일별 가격 스냅샷. PriceService가 업서트함.
| 컬럼 | 유형 | 비고 |
|---|---|---|
| idPK | SERIAL | 자동 생성 |
| stock_symbol | VARCHAR(20) | NOT NULL |
| event_type | stock_event_type | ENUM: split, bonus, merge, delist |
| ratio_num | NUMERIC(18,6) | 승수 분자 |
| ratio_den | NUMERIC(18,6) | 승수 분모 |
| new_symbol | VARCHAR(20) | 합병용 |
| effective_date | TIMESTAMP | NOT NULL |
기업 이벤트. ratio_num/ratio_den이 승수를 정의함 (예: 2:1 분할).
| 컬럼 | 유형 | 비고 |
|---|---|---|
| idPK | SERIAL | 자동 생성 |
| reward_id | INT | FK → rewards(id) ON DELETE CASCADE |
| adjustment_type | VARCHAR(64) | reward_reversal, fee_refund, manual_correction |
| delta_quantity | NUMERIC(18,6) | NOT NULL |
| delta_amount | NUMERIC(18,4) | NOT NULL |
| reason | VARCHAR(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는 없으며 뷰 수준에서 강제됨.
결정: 복잡한 포트폴리오/과거 계산을 애플리케이션 코드 대신 PostgreSQL 뷰로 구현
이유: 애플리케이션 복잡성을 줄이고 PostgreSQL 쿼리 옵티마이저를 활용하며 계산의 단일 진실 공급원을 제공
이점: 모든 엔드포인트에서 일관된 계산, DB가 JOIN과 집계를 효율적으로 처리
트레이드오프: 대규모 데이터셋에서는 뷰가 느려질 수 있고, 애플리케이션 코드보다 디버깅이 어려우며, PostgreSQL에 종속됨
결정: 누적 기업 이벤트 승수를 계산하기 위해 EXP(SUM(LN(ratio))) 사용
이유: 많은 이벤트가 합성될 때 부동소수점 오버플로/언더플로 방지
이점: 수치적으로 안정적이며 임의 개수의 이벤트 처리 가능
트레이드오프: 직접 곱셈보다 읽기 어렵고 양수 비율이 필요함
결정: 비즈니스 로직이 별도 서비스 계층 없이 핸들러에 직접 위치
이유: 소규모 프로젝트에 더 단순하고 유지할 파일과 추상화가 적음
이점: 보일러플레이트가 적고 요청 흐름 추적이 쉬움
트레이드오프: 로직 재사용이 어렵고 HTTP와 데이터 접근의 결합이 강하며 비즈니스 로직을 독립적으로 테스트하기 어려움
결정: 실제 시장 데이터 API 대신 RandomPriceFetcher 사용
이유: 개발/테스트 환경이며 API 키 관리가 필요 없음
이점: 외부 의존성이 전혀 없고 거의 결정론적 동작
트레이드오프: 프로덕션 준비가 아니며 포트폴리오 가치가 의미 없고 실제 배포 시 교체해야 함
결정: 외부 트랜잭션을 중단하지 않고 멱등성을 처리하기 위해 PostgreSQL SAVEPOINT 사용
이유: 외부 멱등성 서비스 없이 재시도 안전성 확보
이점: PostgreSQL 제약 내에서 동작하는 정교한 트랜잭션 내 멱등성
트레이드오프: 복잡한 오류 처리와 SAVEPOINT 오버헤드
결정: 모든 엔드포인트가 인증 없이 공개 접근 가능
이유: 개발/데모 프로젝트이며 인증은 나중에 추가 예정 (예: API 게이트웨이 경유)
이점: 구현이 단순하고 테스트가 쉬움
트레이드오프: 프로덕션에 부적합하며 모든 클라이언트가 리워드를 생성하거나 조정을 적용할 수 있음