ADR-003 paper 격리 방식 선택
상태
결정 (확정)
맥락
토스 Open API에는 sandbox가 없다 (2026-08-27 확인 — 스펙 servers가 프로덕션 1개, sandbox·모의투자·mock 언급 0건). Alpaca처럼 “페이퍼 계정 자격증명을 분리해 실주문이 원천적으로 불가능하게” 만드는 방식을 쓸 수 없다. 같은 토큰·같은 엔드포인트로 실주문이 나간다.
따라서 paper 모드의 안전성은 전적으로 우리 코드에만 의존한다. 이 방어선을 어떻게 세울지 정해야 한다. 실패하면 결과는 의도하지 않은 실주문, 즉 실제 금전 손실이다.
결정
의존성 분리로 격리한다. TradeExecutor 인터페이스의 두 구현을 두고, PaperTradeExecutor는 OrderGateway·ConditionalOrderGateway를 생성자에서 받지 않는다.
interface TradeExecutor {
fun mode(): ExecutionMode
fun buy(proposal: OrderProposal, marketPrice: BigDecimal): TradingFill
fun sell(position: TradingPosition, marketPrice: BigDecimal, reason: SellReason): TradingFill
}
// 토스로 가는 참조가 없다 — 실주문이 "설정 실수로 나갈 수 있는 것"이 아니라 "참조할 대상이 없는 것"
class PaperTradeExecutor(private val costModel: TradingCostModel) : TradeExecutor
class LiveTradeExecutor(
private val orderGateway: OrderGateway,
private val conditionalOrderGateway: ConditionalOrderGateway,
private val costModel: TradingCostModel,
) : TradeExecutor이를 두 가지로 강제한다.
- 리플렉션 테스트 —
PaperTradeExecutor의 생성자 파라미터에 Gateway 타입이 없음을 검증한다. 누군가 나중에 주입하면 테스트가 깨진다. - 사이클 전체 검증 —
paper모드로 전체 사이클을 돌렸을 때 MockWebServer에 도착한 토스 요청 수가 0건임을 검증한다.
근거
- freqtrade가 같은 원리를 쓴다. dry-run 모드에서는 거래소 API 키 자체가 필요 없다 — 실주문 경로가 설정이 아니라 의존성 수준에서 부재한다 (Configuration).
- 런타임 분기는 사람의 주의력에 의존한다.
if (mode == PAPER) return은 집행 지점이 늘 때마다 하나씩 추가해야 하고, 언젠가 하나가 빠진다. 빠진 결과가 실주문이면 그 리스크는 감수할 값이 아니다. - 타입 시스템이 가장 싸다. 컴파일 시점에 막히면 리뷰·테스트·운영 어디에도 부담이 없다.
- PRD FR-35(“
paper에서 토스 주문 API를 호출하는 경로가 존재하지 않음을 테스트로 강제”)의 가장 강한 구현이다.
고려한 대안
| 대안 | 미채택 사유 |
|---|---|
런타임 if 분기 | 집행 지점마다 반복돼야 하고 누락 시 실주문. 유일한 방어선을 사람 주의력에 맡긴다 |
| 별도 자격증명 (더미 토큰) | 토스가 sandbox를 제공하지 않으므로 더미 토큰은 401만 낸다. “실패하니까 안전하다”는 방어는 401이 아닌 응답이 오는 순간 무너진다 |
| 페이퍼 전용 별도 프로세스 | 제안·게이트·포지션 로직이 통째로 중복된다. 배포·운영이 두 배가 되고 두 경로가 갈라진다 |
@ConditionalOnProperty로 빈 토글 | no-conditional-on-property 위반. 재기동 없이 전환·롤백이 안 되고, 두 경로를 한 배포로 검증할 수 없다 |
영향
AutoTradingDomainService는List<TradeExecutor>를 주입받아 현재 정책의 모드에 맞는 구현을 선택한다.LiveTradeExecutor는 M4까지 빈으로 등록되되 호출되지 않는다 — 정책 모드가PAPER인 한 선택되지 않는다. 빈 등록 자체를 토글하지 않으므로 no-conditional-on-property를 지킨다.- 비용 모델(
TradingCostModel)은 두 구현이 공유한다 — 페이퍼 체결가 산출과 실거래 실효가 계산이 같은 공식을 쓴다. - 테스트 부담이 생긴다: 격리 보장 테스트 2종이 상시 유지돼야 한다.