- springboot
- hibernate
- Django
- 프로그래머스
- nginx
- spring security 6
- AWS
- PYTHON
- select
- join
- 자바
- ORM
- spring mvc
- java
- @transactional
- 데이터베이스
- mysql
- jpa
- 스프링부트
- spring boot
- static
- string
- 문자열
- 1차원 배열
- 스프링
- Docker
- spring
- DI
- sql
- SSL
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
개발하는 자몽
[Network] STOMP와 SockJS 본문
Spring 환경에서 STOMP 프로토콜을 사용하여 일대일 실시간 채팅을 구현해야 하는 일이 있었는데 설정에 SockJS를 활성화하도록 추가했다. 클라이언트인 Flutter는 `StompClient` 라이브러리와 `ws` 프로토콜로 서버에 연결 요청을 시도했으나 계속 실패했다. 개발 시간이 촉박해서 제대로 STOMP와 SockJS에 대해 공부하지 않았던 것이 이렇게.. 계속 오류를 뿜어댔다...
아래는 Flutter에서 Spring 서버로 연결 시도 시 실패했던 Spring, Flutter 코드이다.
Spring Boot 3.4.1
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
...
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/stomp") // create connection - ws://domain:port/stomp
.setAllowedOriginPatterns("*")
.withSockJS();
}
}
`registerStompEndpoints`에서 `registry`에 체이닝으로 `withSockJS()`를 추가한 부분이 포인트이다. 해당 함수를 추가하면 `SockJS` 사용이 활성화된다.
Flutter 3.5.4
class ChatService {
static const baseUrl = 'YOUR_BASE_URL'; // e.g., 'http://your-domain:port'
late StompClient stompClient;
final Function(String) onMessageReceived;
final int userId;
ChatService({required this.userId, required this.onMessageReceived}) {
_initializeStompClient();
}
void _initializeStompClient() {
stompClient = StompClient(
config: StompConfig( // 여기를 주목!
url: '${baseUrl.replaceFirst('http', 'ws')}/stomp',
onConnect: onConnect,
onDisconnect: (f) => print('Disconnected'),
onWebSocketError: (dynamic error) => print(error.toString()),
stompConnectHeaders: {'userId': userId.toString()},
webSocketConnectHeaders: {'userId': userId.toString()},
),
);
}
...
`_initializeStompClient()`의 `config` 설정을 `StompConfig()`로 한 것과 `URL`의 `scheme`을 `http`에서 `ws`로 교체하여 서버에 연결을 시도하는 것이 주목해야 하는 점이다.
이번에는 STOMP와 SockJS가 무엇인지 알아보고, 어떻게하면 연결이 성공하는지 알아보자.
STOMP와 SockJS
🔖 프레임, 패킷
https://ict-story.tistory.com/39
프레임(Frame)
- 프레임은 데이터와 제어정보를 합친 것으로, 다양한 프로토콜들에 의해 교환되고 운반되는 데이터 단위를 뜻한다.
- OSI 7계층 중 데이터링크 계층(Data Link layer, Layer 2)에서 정의된다.
- 일반적인 프레임 구성 형태
패킷(Packet)
- 패킷은 데이터를 일정 크기로 자른 것으로, OSI 7 계층 중 네트워크 계층(Network layer, Layer 3)에서 정의되는 데이터 단위이다.
- 그 외에도 OSI의 각 계층에서 주고받는 정보의 단위를 모두 패킷이라고 총칭하기도 한다.
1. 기본 개념
💡`Frame` ↔ `Framing`
SockJS framing은 클라이언트와 서버 간 전송되는 메세지를 다루고 캡슐화하기 위한 전체적인 메커니즘 또는 구조를 의미한다. 통신이 SockJS 프로토콜을 준수하는지 확인하여 다양한 전송 방법(웹소켓, HTTP Streaming, long polling, etc.) 간의 호환성을 지원한다.
다음 문맥에서는 SockJS 프레임(개별 통신 단위)의 형식을 지정, 전송, 해석하는 방법을 제어하는 프로세스 또는 방법론을 의미한다.
SockJS frame은 SockJS 프로토콜 내의 단일 통신 단위이다. 통신을 통해 전송된 실제 메세지나 컨트롤 패킷을 나타내며 다음의 것들이 포함되어 있을 수 있다; Initialization data, Heartbeat signals, Actual application data
프레임은 일반적으로 SockJS의 특정 형식 규칙을 준수한다. 예를 들어, 페이로드를 JSON 형태로 캡슐화하거나 메시지 유형을 구별하기 위해 헤더를 추가하여 규칙을 준수한다.
SockJS framing은 메세지 패키징, 전송 및 해석을 위한 프로토콜 레벨의 프로세스를 설명하는 더 넓은 개념이고, SockJS frame은 framing 규칙에 따라 형식이 지정되고 전송되는 메시지의 특정 인스턴스다.
SockJS
- 웹소켓을 사용할 수 없을 때 대체 매커니즘(fallback mechanism)을 제공하는 웹소켓 에뮬레이션(emulation) 라이브러리이다.
- 따라서 Internet Explorer와 같은 레거시 브라우저나 제한적인 네트워크 환경에서 신뢰할 수 있는 전송을 보장할 수 있다.
- Provided Fallback Mechanism Ex. XHR streaming, long polling, …
- 웹소켓으로 연결을 처음 시도하고, 실패하면 HTTP 기반의 전송으로 대체한다.
- 클라이언트와 서버 간의 호환성을 위해 메세지에 SockJS 관련 프레이밍(framing)을 추가한다.
STOMP
- 웹소켓이나 SockJS와 같은 전송을 통한 구조화된 통신을 위한 메시지 프로토콜이다.
- 애플리케이션 계층에서 메세지 교환 규칙을 정의한다.
2. SockJS와 STOMP가 함께 동작하는 방식
- SockJS: 웹소켓으로 첫 연결을 시도하고, 필요한 경우 HTTP 기반의 전송으로 대체하는 전송 메커니즘을 처리한다.
- STOMP: 구조화된 메세지 호환성을 제공하기 위해 SockJS 또는 웹소켓 위에서 동작한다.
3. Configuration 기반 동작
Case 1. Spring 서버에서 SockJS 활성화
- 예상되는 동작
- 클라이언트가 웹소켓이나 HTTP을 통해서 통신을 하는 것과 상관없이, 서버는 모든 통신에 대해서 SockJS 관련 프레이밍을 예상한다.
- SockJS는 웹소켓으로 통신을 시도하고 실패하면 HTTP 통신으로 대체한다.
- Flutter 클라이언트 시나리오
- SockJS를 활성화하고 `ws` 프로토콜을 사용하는 경우
- 결과: 프로토콜 미스매치로 인한 연결 실패
- 원인: SockJS 프레이밍을 예상했으나, 원시 웹소켓 핸드셰이크가 전송됨
- SockJS를 활성화하고 `http` 프로토콜을 사용하는 경우
- 결과: 성공
- 원인: SockJS는 전송 폴백을 관리하고 서버가 예상한 값에 맞춰 조정
- SockJS를 비활성화하고 `ws` 또는 `http` 프로토콜을 사용하는 경우
- 결과: SockJS 프레이밍의 부재로 인한 실패
- 원인: 서버에서 연결을 거절함
- SockJS를 활성화하고 `ws` 프로토콜을 사용하는 경우
Case 2. Spring 서버에서 SockJS 비활성화
- 예상되는 동작
- 서버가 원시 웹소켓으로 동작한다.
- HTTP 전송과 같은 대체(fallback) 메커니즘을 지원하지 않는다.
- Flutter 클라이언트 시나리오
- SockJS를 비활성화하고 `ws` 프로토콜을 사용하는 경우
- 결과: 성공
- 원인: 직접적인 WebSocket 연결과 서버가 기대한 값이 일치함
- SockJS를 활성화하고 `ws` 또는 `http` 프로토콜을 사용하는 경우
- 결과: 성공
- 원인: SockJS 프레이밍과 서버의 원시 웹소켓 구현이 호환되지 않음
- SockJS를 비활성화하고 `http` 프로토콜을 사용하는 경우
- 결과: 실패
- 원인: 일반 HTTP는 원시 웹소켓 서버에서 지원되지 않음
- SockJS를 비활성화하고 `ws` 프로토콜을 사용하는 경우
4. 특정 Configuration의 성공 또는 실패 이유
✅ Flutter에서 SockJS 활성화 시 `ws` 연결이 실패하는 이유
- SockJS를 활성화한 클라이언트는 SockJS 프레이밍이 포함된 핸드셰이크를 전송한다.
- 원시 `ws://` 요청은 SockJS 협상(negotiate)을 우회하여, SockJS를 활성화한 Spring 서버와의 프로토콜 미스매치를 발생시킨다.
✅ Flutter에서 SockJS 활성화 시, `http` 연결이 성공하는 이유
- SockJS는 대체 매커니즘의 일부인 `http://`를 사용한다.
- 클라이언트는 SockJS가 전송을 협상하고 서버와 조율할 수 있도록 한다.
💡 위 문맥에서 협상(negotiate)의 의미
클라이언트와 서버 간의 통신에 가장 적합한 전송 방법을 결정하고 합의하는 과정
Step 1. 최선의 옵션으로 시작하기 (웹소켓)
SockJS는 첫 연결 시도를 웹소켓으로 한다. 이는 가장 효과적이고 선호되는 전송 방법이기 때문이다.
Step 2. 필요하면 대체하기
웹소켓이 안된다면 SockJS는 `HTTP streaming`이나 `long polling`과 같은 전송 방법들을 시도한다.
이때 웹소켓 연결 실패 원인에는 네트워크 제한, 오래된 브라우저, 프록시 등이 있다.
Step 3. 전송 방법 확정하기
SockJS는 사용할 수 있는 최선의 방법을 결정하고 해당 방법을 통신에 사용하도록 클라이언트와 서버를 모두 조정한다.
위 내용을 바탕으로 다시 설정을 해보자.
Spring
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
...
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/stomp") // create connection - ws://domain:port/stomp
.setAllowedOriginPatterns("*")
.withSockJS();
}
}
처음과 동일하게 SockJS를 활성화시킨 코드이다.
Flutter
class ChatService {
static const baseUrl = 'YOUR_BASE_URL'; // e.g., 'http://your-domain:port'
late StompClient stompClient;
final Function(String) onMessageReceived;
final int userId;
ChatService({required this.userId, required this.onMessageReceived}) {
_initializeStompClient();
}
void _initializeStompClient() {
stompClient = StompClient(
config: StompConfig.sockJS( // sockJS 추가됨
url: '$baseUrl/stomp', //http 또는 https 사용
onConnect: onConnect,
onDisconnect: (f) => print('Disconnected'),
onWebSocketError: (dynamic error) => print(error.toString()),
stompConnectHeaders: {'userId': userId.toString()},
webSocketConnectHeaders: {'userId': userId.toString()},
),
);
}
...
처음 코드와 달리 `StompConfig`에 `sockJS`가 추가되었고, URL도 `ws`로 대체하는 것이 아닌 `http`를 그대로 사용하도록 했다.
처음 Flutter 코드처럼 `sockJS`를 사용하지 않을 거라면 Spring 설정에서 `withSockJS()`를 제거하고, Flutter URL의 scheme도 `ws`를 사용하면 된다.
연결 성공! 🥳
'개발 지식' 카테고리의 다른 글
[TIL / Design Pattern] 프록시 패턴, 데코레이터 패턴 (0) | 2023.03.27 |
---|---|
[TIL / Design Pattern] 템플릿 메서드 패턴, 전략 패턴(템플릿 콜백 패턴) (0) | 2023.03.24 |
[TIL] 클라이언트 검증, 서버 검증 (0) | 2023.03.09 |
[TIL] 백엔드 아키텍처, Nginx (0) | 2023.01.19 |
[OS] 동기와 비동기, 블로킹과 논블로킹 (0) | 2023.01.18 |