client와 웹소켓 서버 사이를 중계하기 = 웹소켓 서버이면서 클라이언트 역할을 수행한다.
server1과 server2를 어떤 방법으로 어느 타이밍에 연결할지에 고민이 많았다.
방법에 있어서는 server2의 WebSocket Server는 다룰 수 없고 server2는 Stomp 프로토콜을 사용하고 있지 않은데 server1을 Stomp Client로 server2와 연결을 시도하며 삽질했고 타이밍에 있어서는 요청을 트리거로 server1과 server2를 연결하고 작업완료 시 연결을 종료하려 했으나 잘 안 됐었다.
결국, 구현은 server1과 server2의 ws연결은 spring websocket stomp 가 아닌 java-websocket 라이브러리를 사용했고
implementation 'org.java-websocket:Java-WebSocket:1.5.4'
타이밍은 애플리케이션이 시작하면 연결하고 연결된 상태를 계속 유지하게 했다.
지금 생각해 보면 타이밍은 전자(req 트리거로 연결하고 끊기)의 방법도 가능하다.
(처음에는 req가 묶여서 tomcat thread pool의 thread가 잡혀있지 않을까 싶었는데, 그건 잘못된 방법으로 연결한 것이다.)
다만, server2의 WebSocket Server를 다룰 수 없고 Stomp처럼 특정 라우트에 pub/sub을 할 수 있지도 않다. 그래서 모든 요청은 같은 ws 채널에 연결되고 받는 message도 같다. 요청 => 연결 후 받은 메시지에 대한 비즈니스 로직이 다르지도 않다. 그래서 굳이 요청마다 연결하는 세션을 늘릴 이유는 없다고 판단했다.
구조
정보를 노출하지 않고 작업 스케줄을 추가/관리/기록해야 하는 이유로 client는 server2의 웹소켓에 직접 연결할 수 없다.
그래서 server1은 server2와 client 사이에서 웹소켓 server/client를 동시에 수행하며 중계 역할을 해야 한다.
WebSocket Stomp Client/Server 와 WebSocket Client/Server의 연결 흐름을 보면 위와 같다.
순서
ws server/client를 동시에 수행하며 중계 역할을 하기 위한 순서는 다음과 같다.
- Spring Stomp Server가 올라간다. client는 server1에 Stomp Client를 연결한다. (client <=> server1)
- server1은 올라간 Stomp Server에 Stomp Client를 연결한다. (server1 <=> server1)
- Stomp Client가 연결된 후에 WebSocket Server에 WebSocket Client를 연결한다. (server1 <=> server2)
- WebSocket Client는 message를 받을 때, Stomp Client 입장으로 Stomp Server에 pub을 한다.
server1의 Stomp Server 역할이 먼저 올라가야 모든 연결이 가능하다.
Stomp Client를 연결할 때는 ApplicationRunner 인터페이스를 구현하여 bean이 올라간 후에 연결을 보장할 수 있다.
코드
1. Stomp 설정
@RequiredArgsConstructor
@Configuration
@EnableWebSocketMessageBroker
public class StompConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/sub");
registry.setApplicationDestinationPrefixes("/pub");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws-stomp").setAllowedOriginPatterns("*");
}
}
@RestController
@RequiredArgsConstructor
@Slf4j
public class StompController {
@Async
@MessageMapping("/목적지")
public void test(String s) {
//client에 전송할 로직
...
}
}
2. Spring bean들이 먼저 올라간 후(= Stomp Server가 실행된 후) StompClient를 연결한다.
@Component
@Slf4j
@RequiredArgsConstructor
public class SocketThread implements ApplicationRunner {
//bean들이 올라간 후 실행된다. (spring boot가 실행된 후 실행된다.)
...
@Override
public void run(ApplicationArguments args) {
WebSocketStompClient stompClient = new WebSocketStompClient(new StandardWebSocketClient());
StompSessionHandler sessionHandler = new CustomStompSessionHandler(...);
stompClient.connectAsync(wsEndPointUrl, sessionHandler);
}
}
3. StompClient가 열린 후 WebSocketClient를 연결한다.
public class CustomStompSessionHandler implements StompSessionHandler {
....
@Override
public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
....
CustomWebSocketClient customWebSocketClient = new CustomWebSocketClient(url, new Draft_6455(), session, destination);
try {
customWebSocketClient.connectBlocking(); // server2에 ws연결
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
....
}
4. server2로부터 message를 받으면 Stomp Client 입장으로 Stomp Server에 pub을 한다.
public class CustomWebSocketClient extends WebSocketClient {
private StompSession session;
private String destination;
....
public CustomWebSocketClient(URI serverUri, Draft protocolDraft, StompSession session, String destination) {
super(serverUri, protocolDraft);
this.session = session;
....
}
@Override
public void onMessage(String message) {
// server2로 부터 message를 받으면 stomp server에 pub을 한다.
session.send(destination, message.getBytes());
....
}
....
}
'Spring' 카테고리의 다른 글
Spring RestTemplate - 공공api SERVICE KEY IS NOT REGISTERED ERROR (0) | 2024.10.25 |
---|---|
Spring Stomp로 보는 직렬화/역직렬화 (0) | 2024.07.05 |
spring mapstruct (0) | 2023.10.15 |
spring lombok SuperBuilder (0) | 2023.10.08 |
spring mongodb repository (0) | 2023.10.01 |