목표가 알림 TDD (Technical Design Document)
Background
PRD 참조. 토스 시세를 주기 폴링해 목표가 도달을 감지하고 멀티 채널로 1회 발송한다. Kotlin/Spring Boot, Hexagonal Architecture + Rich Domain Model.
Overview
alert/watchlist/stock/common4개 도메인 패키지.- 폴링 스케줄러(presentation) → UseCase → DomainService → 토스 Gateway·Notification Gateway·Repository.
- 알림 발송은 Composite Gateway로 Discord·Mac을 동시 호출.
Terminology
PRD의 Terminology를 따른다. 추가:
| 용어 | 정의 |
|---|---|
| CompositeNotificationGateway | 등록된 알림 채널을 모두 호출하는 @Primary 게이트웨이 |
| TossPriceGateway | 토스 OAuth·시세 호출을 담당하는 domain Gateway 구현 |
Define Problem
AS-IS
- 신규 프로젝트. 알림 감시·발송 인프라 없음.
TO-BE
- Hexagonal 레이어로 분리: presentation(Controller·Scheduler) → application(UseCase) → domain(Service·Entity·Gateway interface) ← infrastructure(구현).
Possible Solutions
방안 비교
| 방안 | 설명 | 채택 | 미채택 사유 |
|---|---|---|---|
| 주기 폴링 + 스케줄러 | @Scheduled로 N초마다 ACTIVE 알림 일괄 평가 | ✅ | — |
| WebSocket 실시간 구독 | 시세 push 구독 | ✗ | 토스 API가 WebSocket·push 미제공 |
| 채널별 개별 호출 | UseCase가 Discord·Mac을 각각 호출 | ✗ | 채널 추가 시 변경 전파. Composite로 캡슐화 |
Detail Design
Component Diagram
flowchart LR subgraph Presentation["Presentation"] Controller[AlertApiController] Scheduler[PriceAlertScheduler] end subgraph Application["Application"] UseCase[Create/Get/Delete UseCase] end subgraph Domain["Domain"] Service[PriceAlertDomainService] Entity[PriceAlert] PGW[PriceGateway] NGW[NotificationGateway] Repo[AlertRepository] end subgraph Infra["Infrastructure"] Toss[TossPriceGateway] Composite[CompositeNotificationGateway] RepoImpl[AlertRepositoryImpl] end Controller --> UseCase Scheduler --> UseCase UseCase --> Service Service --> Entity Service --> PGW Service --> NGW Service --> Repo Toss -.->|implements| PGW Composite -.->|implements| NGW RepoImpl -.->|implements| Repo
Sequence Diagram — 폴링 평가
sequenceDiagram participant S as PriceAlertScheduler participant U as EvaluateAlertsUseCase participant D as PriceAlertDomainService participant T as PriceGateway participant N as NotificationGateway participant R as AlertRepository S->>U: poll() (30초 주기) U->>D: evaluateActiveAlerts() D->>R: findActive() D->>T: getPrices(symbols) T-->>D: prices loop 도달한 알림 D->>N: notify(message) D->>R: save(TRIGGERED) + history end
ERD
erDiagram PRICE_ALERT { bigint id PK varchar symbol decimal target_price varchar direction varchar status datetime created_at datetime triggered_at } ALERT_HISTORY { bigint id PK bigint alert_id varchar symbol varchar direction decimal target_price decimal triggered_price datetime triggered_at } WATCHLIST { bigint id PK varchar symbol UK datetime added_at } STOCK { varchar symbol PK varchar name varchar market }
- 인덱스:
price_alert.idx_status,alert_history.idx_triggered_at·idx_alert_id,watchlist.uk_symbol,stock.idx_name. - FK 컬럼은 두지 않고 애플리케이션 레벨에서 관리 (
alert_history.alert_id는 단순 참조 컬럼).
상태 전이
ACTIVE ──목표 도달──▶ TRIGGERED (터미널)
ACTIVE ──사용자──▶ DISABLED
DISABLED ──재활성화──▶ ACTIVE
도달 판정: ABOVE → currentPrice >= targetPrice, BELOW → currentPrice <= targetPrice. 판정·전이는 PriceAlert Entity 내부에 캡슐화.
Testing Plan
PriceAlertEntity: ABOVE/BELOW 도달 판정, ACTIVE→TRIGGERED 전이 규칙, 중복 trigger 방지 검증PriceAlertDomainService: ACTIVE 알림 일괄 평가, 도달 시 발송 1회·이력 생성, 미도달 시 발송 없음CreatePriceAlertUseCase: 생성 커맨드 → DomainService 위임 흐름 단위 검증AlertRepositoryImpl+TossPriceGateway: TestContainers(MySQL) DB 영속화 검증, 토큰 갱신·401 재시도 검증AlertApiController: MockMvc로 POST/GET/DELETE 엔드포인트 통합 검증- 프레임워크: Kotest(BehaviorSpec) + MockK + TestContainers
Release Scenario
docker compose up -d(MySQL 3308).- Flyway 마이그레이션 자동 적용 (
price_alert,alert_history,watchlist,stock). - 환경변수(
TOSS_API_KEY,TOSS_SECRET_KEY, 선택 Discord) 주입 후./gradlew bootRun. - 롤백: 스케줄러 비활성(
alert.polling미설정/프로퍼티 OFF), 마이그레이션 역방향 DDL로 테이블 제거.
Observability
- 폴링 1회당 평가 알림 수·발송 수 로그.
- 토스 API 호출 실패·토큰 갱신 로그.
Project Information
- 담당: biuea
- 스택: Kotlin / Spring Boot / Hexagonal Architecture / MySQL 8.0 / Flyway
- DB 포트: 3308 (Docker)
- 알림 채널: Discord Webhook + Mac osascript