Skip to content

Commit 1da0209

Browse files
SimoKiiclaude
andcommitted
Refactor: 거래소 확장을 위한 커넥터 인터페이스 및 설정 구조 추가
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7f580d6 commit 1da0209

4 files changed

Lines changed: 295 additions & 0 deletions

File tree

src/main/java/com/rabbittick/streamer/MarketIngestStreamerApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
56
import org.springframework.scheduling.annotation.EnableScheduling;
67

78
@SpringBootApplication
89
@EnableScheduling
10+
@ConfigurationPropertiesScan
911
public class MarketIngestStreamerApplication {
1012

1113
public static void main(String[] args) {
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package com.rabbittick.streamer.config;
2+
3+
import java.net.URI;
4+
import java.time.Duration;
5+
import java.util.LinkedHashMap;
6+
import java.util.List;
7+
import java.util.Map;
8+
9+
import org.springframework.boot.context.properties.ConfigurationProperties;
10+
11+
import lombok.Getter;
12+
import lombok.Setter;
13+
14+
/**
15+
* 다중 거래소 WebSocket 커넥터 설정을 바인딩하는 프로퍼티 클래스.
16+
*
17+
* <p>{@code application.yml}의 {@code exchanges.configs} 하위 설정을 거래소 식별자를
18+
* 키로, {@link ExchangeConfig}를 값으로 하는 맵에 바인딩한다.
19+
* 거래소 식별자는 YAML 키(소문자)이며, 커넥터 내부에서 대문자로 변환하여 사용한다.
20+
*
21+
* <p>YAML 설정 예시:
22+
* <pre>{@code
23+
* exchanges:
24+
* configs:
25+
* upbit:
26+
* enabled: true
27+
* websocket-uri: wss://api.upbit.com/websocket/v1
28+
* ping-interval-seconds: 60
29+
* max-markets-per-connection: 100
30+
* processing-concurrency: 32
31+
* environment: fetched
32+
* market-prefixes: [KRW, BTC, USDT]
33+
* data-types:
34+
* ticker: true
35+
* trade: true
36+
* orderbook: true
37+
* binance:
38+
* enabled: false
39+
* websocket-uri: wss://stream.binance.com:9443/ws
40+
* market-prefixes: [USDT]
41+
* data-types:
42+
* ticker: true
43+
* trade: false
44+
* orderbook: false
45+
* }</pre>
46+
*
47+
* <p>새로운 거래소를 추가하려면 {@code exchanges.configs} 하위에 거래소 식별자를 키로
48+
* 새 항목을 추가하고, 해당 {@link com.rabbittick.streamer.connector.ExchangeConnector}
49+
* 구현체를 작성한다.
50+
*/
51+
@Getter
52+
@Setter
53+
@ConfigurationProperties("exchanges")
54+
public class ExchangeProperties {
55+
56+
/**
57+
* 거래소 식별자(소문자)를 키로, 개별 거래소 설정을 값으로 가지는 맵.
58+
*
59+
* <p>키는 YAML에 선언된 소문자 거래소 이름이다.
60+
* 커넥터에서 라우팅 키 생성 시 {@code String.toUpperCase()}로 변환하여 사용한다.
61+
*/
62+
private Map<String, ExchangeConfig> configs = new LinkedHashMap<>();
63+
64+
/**
65+
* 단일 거래소의 WebSocket 연결 및 수집 설정을 담는 클래스.
66+
*/
67+
@Getter
68+
@Setter
69+
public static class ExchangeConfig {
70+
71+
/**
72+
* 이 커넥터의 활성화 여부.
73+
*
74+
* <p>{@code false}이면 {@code ConnectorManager}가 {@code start()} 호출 없이
75+
* 건너뛴다. 기본값은 {@code false}로, 명시적으로 활성화해야 연결이 시작된다.
76+
*/
77+
private boolean enabled = false;
78+
79+
/**
80+
* 거래소 WebSocket 서버 URI.
81+
*
82+
* <p>예: {@code wss://api.upbit.com/websocket/v1}
83+
*/
84+
private URI websocketUri;
85+
86+
/**
87+
* 연결 유지를 위한 Ping 전송 주기 (초).
88+
*
89+
* <p>거래소별 권장 주기에 맞게 설정한다. 기본값은 60초.
90+
*/
91+
private int pingIntervalSeconds = 60;
92+
93+
/**
94+
* 단일 WebSocket 연결당 최대 구독 마켓코드 수.
95+
*
96+
* <p>이 값을 초과하면 마켓코드를 청크로 나눠 복수의 연결을 유지한다.
97+
* 거래소 API 제약에 맞게 조정한다. 기본값은 100.
98+
*/
99+
private int maxMarketsPerConnection = 100;
100+
101+
/**
102+
* 메시지 처리 시 동시에 실행할 내부 Mono의 최대 개수.
103+
*
104+
* <p>파싱·변환·발행 작업은 {@code boundedElastic} 스케줄러에서
105+
* 이 값을 상한으로 병렬 처리된다. 기본값은 32.
106+
*/
107+
private int processingConcurrency = 32;
108+
109+
/**
110+
* 사용할 마켓코드 환경 이름.
111+
*
112+
* <p>마켓코드 YAML 파일 내 {@code env} 섹션의 키와 일치해야 한다.
113+
* 예: {@code development}, {@code production}, {@code fetched}.
114+
* 기본값은 {@code development}.
115+
*/
116+
private String environment = "development";
117+
118+
/**
119+
* 구독할 마켓 prefix 목록.
120+
*
121+
* <p>각 prefix에 해당하는 마켓코드를 {@code loadMarketCodes()}에서 로드한다.
122+
* 예: {@code [KRW, BTC, USDT]}.
123+
* 기본값은 {@code [KRW]}.
124+
*/
125+
private List<String> marketPrefixes = List.of("KRW");
126+
127+
/**
128+
* 수집할 데이터 타입별 활성화 설정.
129+
*
130+
* <p>비활성화된 타입의 메시지는 수신하더라도 무시한다.
131+
* 최소 하나의 타입이 활성화되어 있어야 한다.
132+
*/
133+
private DataTypesConfig dataTypes = new DataTypesConfig();
134+
135+
/**
136+
* {@code pingIntervalSeconds}를 {@link Duration}으로 변환하여 반환한다.
137+
*
138+
* <p>{@link com.rabbittick.streamer.connector.ExchangeConnector#getPingInterval()}
139+
* 구현체에서 이 메서드를 사용하여 Ping 주기를 가져온다.
140+
*
141+
* @return Ping 전송 주기
142+
*/
143+
public Duration getPingInterval() {
144+
return Duration.ofSeconds(pingIntervalSeconds);
145+
}
146+
}
147+
148+
/**
149+
* 수집할 데이터 타입별 활성화 여부를 담는 클래스.
150+
*/
151+
@Getter
152+
@Setter
153+
public static class DataTypesConfig {
154+
155+
/** 현재가(ticker) 데이터 수집 활성화 여부. 기본값은 {@code true}. */
156+
private boolean ticker = true;
157+
158+
/** 거래 체결(trade) 데이터 수집 활성화 여부. 기본값은 {@code false}. */
159+
private boolean trade = false;
160+
161+
/** 호가(orderbook) 데이터 수집 활성화 여부. 기본값은 {@code false}. */
162+
private boolean orderbook = false;
163+
}
164+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package com.rabbittick.streamer.connector;
2+
3+
import java.net.URI;
4+
import java.time.Duration;
5+
import java.util.List;
6+
7+
import org.springframework.web.reactive.socket.WebSocketSession;
8+
9+
import reactor.core.publisher.Mono;
10+
11+
/**
12+
* 거래소 WebSocket 커넥터의 표준 계약을 정의하는 인터페이스.
13+
*
14+
* <p>주요 책임:
15+
* <ul>
16+
* <li>거래소별 연결 정보(URI, Ping 주기) 제공</li>
17+
* <li>WebSocket 세션에 거래소 전용 구독 메시지 전송</li>
18+
* <li>수신된 원시 JSON 메시지 파싱 및 서비스 계층 전달</li>
19+
* <li>구독 대상 마켓코드 목록 로드</li>
20+
* </ul>
21+
*
22+
* <p>각 거래소 구현체는 이 인터페이스를 통해 거래소별 프로토콜 차이를
23+
* 캡슐화하고, {@code AbstractExchangeConnector}가 제공하는 공통 연결
24+
* 생명주기(연결 시작, 청크 분할, Ping, 재연결) 위에서 동작한다.
25+
*
26+
* <p>새로운 거래소를 추가하려면 이 인터페이스와
27+
* {@code AbstractExchangeConnector}를 함께 구현한다.
28+
*/
29+
public interface ExchangeConnector {
30+
31+
/**
32+
* 거래소 식별자를 반환한다.
33+
*
34+
* <p>반환값은 RabbitMQ 라우팅 키의 첫 번째 세그먼트로 사용된다.
35+
* 예: {@code "UPBIT"} → 라우팅 키 {@code upbit.ticker.KRW-BTC}
36+
*
37+
* @return 대문자 거래소 식별자 (예: "UPBIT", "BINANCE")
38+
*/
39+
String getExchangeName();
40+
41+
/**
42+
* 거래소 WebSocket 서버 URI를 반환한다.
43+
*
44+
* @return WebSocket 서버 URI
45+
*/
46+
URI getWebSocketUri();
47+
48+
/**
49+
* 연결 유지를 위한 Ping 전송 주기를 반환한다.
50+
*
51+
* <p>거래소마다 권장 Ping 주기가 다를 수 있으므로 구현체가 직접 정의한다.
52+
*
53+
* @return Ping 전송 주기
54+
*/
55+
Duration getPingInterval();
56+
57+
/**
58+
* 이 커넥터의 활성화 여부를 반환한다.
59+
*
60+
* <p>{@code false}를 반환하는 커넥터는 {@code ConnectorManager}에 의해
61+
* {@link #start()} 호출 없이 건너뛴다.
62+
*
63+
* @return 활성화 여부
64+
*/
65+
boolean isEnabled();
66+
67+
/**
68+
* WebSocket 연결을 시작하고 데이터 스트리밍을 개시한다.
69+
*
70+
* <p>{@code ConnectorManager}가 {@code ApplicationReadyEvent} 수신 후
71+
* 활성화된 커넥터에 대해 호출한다. 연결 실패 시 지수 백오프 방식으로
72+
* 재연결을 시도하는 것은 {@code AbstractExchangeConnector}의 공통 구현에서 처리한다.
73+
*/
74+
void start();
75+
76+
/**
77+
* WebSocket 세션에 거래소 전용 구독 메시지를 전송한다.
78+
*
79+
* <p>거래소마다 구독 메시지 포맷이 다르므로 구현체가 직접 정의한다.
80+
* 예: Upbit는 {@code [{"ticket":…}, {"type":"ticker","codes":[…]}]} 형태의 JSON을 전송한다.
81+
*
82+
* @param session 데이터를 수신할 WebSocket 세션
83+
* @param marketCodes 구독할 마켓코드 목록
84+
* @return 구독 메시지 전송 완료를 나타내는 Mono
85+
*/
86+
Mono<Void> sendSubscription(WebSocketSession session, List<String> marketCodes);
87+
88+
/**
89+
* 수신된 원시 JSON 메시지를 파싱하고 처리한다.
90+
*
91+
* <p>메시지 타입 식별 방식은 거래소마다 다르므로 구현체가 직접 정의한다.
92+
* 예: Upbit는 {@code "ty"} 필드로 {@code ticker} / {@code trade} / {@code orderbook}을 구분한다.
93+
*
94+
* <p>파싱할 수 없는 메시지(연결 수락 응답 등)는 빈 Mono를 반환하여 무시한다.
95+
*
96+
* @param rawJson WebSocket으로부터 수신된 원시 JSON 문자열
97+
* @return 처리 완료를 나타내는 Mono. 파싱 불가 메시지의 경우 빈 Mono
98+
*/
99+
Mono<Void> handleMessage(String rawJson);
100+
101+
/**
102+
* 구독할 마켓코드 목록을 로드한다.
103+
*
104+
* <p>설정 파일 또는 외부 API에서 마켓코드를 조회하는 방식은
105+
* 구현체가 직접 정의한다. 로드된 목록은 {@code AbstractExchangeConnector}의
106+
* 청크 분할 로직에 의해 연결당 최대 구독 수 이하로 나뉘어 처리된다.
107+
*
108+
* @return 구독할 마켓코드 목록
109+
*/
110+
List<String> loadMarketCodes();
111+
}

src/main/resources/application.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,24 @@ upbit:
3131
orderbook:
3232
enabled: true
3333

34+
exchanges:
35+
configs:
36+
upbit:
37+
enabled: true
38+
websocket-uri: wss://api.upbit.com/websocket/v1
39+
ping-interval-seconds: 60
40+
max-markets-per-connection: 100
41+
processing-concurrency: 32
42+
environment: fetched
43+
market-prefixes:
44+
- KRW
45+
- BTC
46+
- USDT
47+
data-types:
48+
ticker: true
49+
trade: true
50+
orderbook: true
51+
3452
rabbitmq:
3553
exchange:
3654
market-data: market-data.exchange

0 commit comments

Comments
 (0)