오늘은 웹소켓통신의 연결관리에 대해 공부해보려고 한다.
이번에 채팅 사이트를 개발하게 되면서 https프로토콜과 웹소켓프로토콜을 둘다 사용하면서 느낀점은 http와는 다르게 웹소켓은 연결이 계속 되어 있다보니 연결 이후 처리나 관리의 노하우가 필요하다고 생각됐다.
| HTTP/HTTPS | WebSocket |
| 단방향 통신 클라이언트가 요청을 보내고서버가 응답을 반환 |
양방향 통신 클라이언트와 서버 간에 실시간 양방향 데이터 전송이 가능 |
| 상태 비유지 (stateless) 요청과 응답은 독립적이며, 이전 요청 상태를 기억하지 않는다. |
상태 유지 (stateful) 연결이 지속되며, 상태를 유지할 수 있다. |
| 헤더부담 각 요청마다 헤더 정보를 포함해 오버헤드가 클 수 있다. |
오버헤드 감소 초기 핸드셰이크 후 헤더가 아닌 간단한 프레임으로 데이터 교환 |
| 일회성 연결 요청마다 연결을 열고 닫는다. |
지속적 연결 클라이언트와 서버간 연결을 유지해 효율적인 데이터 전송 가능 |
| 애플리케이션 계층에서 동작 | 애플리케이션 계층에서 동작 |
표로 다시한번 두 통신 프로토콜의 차이를 비교해보았다.
개발 당시 Https로 되어진 기능들은 로그인 성공 시 jwt를 생성하여 발급했고, 클라이언트는 localStorage따위에 저장해두고,
필요에 따라 요청 헤더에 담아 서버에 전달하여 요청마다 독립적으로 인가 (Authorization)을 받게되는 구조였다.
연결의 맺고 끊음이 요청마다 독립적이므로 따로 연결에 대한 관리를 하지 않아도 된다.
그러다 채팅기능을 구현하면서 웹소켓연결을 사용하다보니 이 연결관리를 어떻게 해주는 것이 좋을지 고민이 된 것이다.
연결 수명주기 관리
- 유지관리
기본적으로 서버는 클라이언트와 연결을 지속적으로 유지하며, 클라이언트가 명시적으로 닫거나 네트워크 오류로 연결이 종료되기 전까지는 지속시킨다. (팩트 검증 필요) - 종료 관리
클라이언트 또는 서버가 연결을 닫을 수 있고, 정상 종료시 close 이벤트를 통해 리소스를 해제한다. - 연결 제한
서버는 동시 연결 수를 제한해야 한다. - 헬스 체크
서버는 정기적으로 핑 메시지를 보내 연결 상태를 확인하고 비정상적인 연결은 끊도록 한다. - 재연결
클라이언트 측에서는 연결이 끊어질 경우 재연결 로직을 구현할 필요가 있다. - 로드 밸런싱
여러 서버로 웹소켓 연결을 분산하기 위해 로드 밸런서를 사용한다. - 수평 확장
웹소켓 연결을 여러 노드에 분산시켜 서버 부하를 줄인다. - 백프레셔(Backpressure)
서버가 처리할 수 없는 메시지가 몰릴 경우, 백프레셔 메커니즘으로 속도를 조절한다.
NestJS에서의 웹소켓
1. main.ts
(NestJS 애플리케이션의 진입점(entrypoint)역할을 하는 파일이며, 애플리케이션을 부트스트랩(시작)하고 설정을 초기화하는 중요파일)
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors({
origin: ['*', 'http://localhost:3001'],
methods: ['GET', 'POST', 'PATCH', 'PUT', 'DELETE'],
credentials: true,
});
app.useWebSocketAdapter(new IoAdapter(app));
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
@WebSocketGateway({
namespace: 'chat',
cors: {
origin: '*',
methods: ['GET', 'POST'],
credentials: true,
},
})
위에선 로컬에서 프론트까지 개발을 병행하며 작업해야해서 모든 도메인을 허용해두었다.
실제 서비스에서는 특정 도메인과 포트만 허용하도록 해야한다.
IoAdapter는 Socket.IO와 NestJS 간의 통합을 가능하게 하며, WebSocket을 사용한 실시간 통신 기능을 제공한다.
웹소켓에서 CORS가 중요한 이유? -> "보안"
웹소켓은 클라이언트와 서버가 연결 할 때 HTTP Upgrade를 사용해서 웹소켓 프로토콜로 전환하는데,
이때 초기 요청에서 브라우저는 CORS 정책을 적용하게 된다.
- CSRF (Cross-Site Request Forgery)
악의적인 사이트가 사용자의 브라우저를 통해 웹소켓 연결을 남용- 공격방법
정상적인 애플리케이션에 로그인한 유저 ( 토큰이나 세션 쿠키를 가지고 있음) 를 악성 사이트로 유도하여 동일한 인증 정보를 통해 내 애플리케이션에 웹소켓 연결을 시도하게 됌.
사용자의 계정을 통해 데이터를 조회하거나, 데이터베이스를 조작하게 됌. - 해결
cors 설정을 잘해서 정해진 도메인으로의 접속만 허용하면 됌
- 공격방법
- DoS (Denial of Service)
- 공격방법
CORS가 제대로 설정되지 않은 웹소켓 서버에 대량의 요청을 보내서 리소스 고갈을 시켜 서버를 다운시킬 수 있다. - 해결
모든 도메인 ( * ) 허용하지 않을 것
웹소켓 연결에 대한 인증 및 권한 검사를 수행할 것
동일 IP에서 지나치게 많은 연결을 시도하면 차단하는 등의 방어책을 마련할 것 - 인증된 사용자라고 하더라도 특정시간 내 발송 가능한 메세지 수를 제한하는 방식 등으로 공격을 대비할 것.
- 공격방법
2. gateway.ts
NestJS에서 웹소켓 통신을 관리하는데 사용되는 핵심 개념.
웹소켓을 사용한 실시간 통신을 위한 서비스로서 클라이언트가 연결을 요청하면 해당 클라이언트와 지속적으로 데이터를 주고받을 수 있는 연결을 설정하고, 이를 관리하는 역할을 한다.
(https://docs.nestjs.com/websockets/gateways)
- onModuleInit() 메서드
- Nest에서 관리하는 라이프사이클
- 이벤트가 발생할 때 동작할 수 있는 기능
- https://docs.nestjs.com/fundamentals/lifecycle-events
Documentation | NestJS - A progressive Node.js framework
Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea
docs.nestjs.com
NestJS는 Socket.IO와의 통합을 통해, WebSocket 서버에 이벤트가 발생할 때 해당 이벤트에 대응하는 메서드를 자동으로 호출한다.
- handleConnection(client: Socket)
- 클라이언트가 WebSocket 서버에 연결될 때 호출한다.
클라이언트는 서버에 연결하면, connection 이벤트가 발생하고, 이 이벤트에 대해 NestJS는 자동으로 handleConnection 메서드를 호출.
- 클라이언트가 WebSocket 서버에 연결될 때 호출한다.
- handleDisconnect(client: Socket)
- 클라이언트가 WebSocket 서버에서 연결을 끊을 때 호출된다.
클라이언트가 연결을 종료하면, disconnect 이벤트가 발생하고, NestJS는 자동으로 handleDisconnect 메서드를 호출.
- 클라이언트가 WebSocket 서버에서 연결을 끊을 때 호출된다.
- @SubscribeMessage('message')
- 클라이언트가 특정 이벤트 이름(예: 'message')을 보내면 해당 메서드를 호출한다.
클라이언트가 message 이벤트를 보내면, NestJS는 자동으로 해당 메서드를 호출하여 클라이언트의 메시지를 처리.
- 클라이언트가 특정 이벤트 이름(예: 'message')을 보내면 해당 메서드를 호출한다.
WebSocket 연결 종료 처리
NestJS는 Socket.IO를 내부적으로 사용하여 WebSocket 서버를 구현하고 있다.
클라이언트와 서버 간의 WebSocket 연결은 일반적으로 open, message, close 이벤트를 처리하는 방식으로 이루어진다.
이때, 연결 종료(disconnect)는 Socket.IO에서 연결이 끊어졌다고 판단하는 여러 가지 상황에 의해 결정된다.
1. Socket.IO에서 연결 종료를 판단하는 방식
Socket.IO에서는 클라이언트와 서버 간의 연결 상태를 관리하고, 이를 disconnect 이벤트로 처리하며 연결이 끊어지는 이유는 여러 가지가 있을 수 있다.
Socket.IO는 클라이언트가 연결을 끊을 때 disconnect 이벤트를 발생시키고, 이를 NestJS의 WebSocket Gateway 내에서 자동으로 처리할 수 있도록 연결해준다.
- 클라이언트가 연결을 종료
클라이언트가 socket.disconnect()를 호출하거나, 클라이언트 브라우저가 종료되거나, 페이지를 새로 고침하는 등의 이유로 연결이 끊어질 수 있다. - 서버가 연결을 종료
서버 측에서 socket.disconnect()를 호출하여 클라이언트의 연결을 종료시킬 수도 있다. - 네트워크 문제
클라이언트와 서버 간의 네트워크 문제로 인해 연결이 끊어질 수 있다.
예를 들어, 클라이언트가 인터넷 연결을 끊거나 서버가 네트워크 장애를 겪을 경우 연결이 종료됌. - 클라이언트 타임아웃
일정 시간 동안 클라이언트와 서버 간에 메시지가 교환되지 않으면, ping/pong 체크를 통해 서버가 클라이언트의 연결을 종료할 수도 있다.
'BackEnd' 카테고리의 다른 글
| nestJS 실시간 채팅 앱 - [9] logging (winston) (0) | 2024.12.09 |
|---|---|
| 서버 모니터링 (feat. prometheus, grafana) (0) | 2024.12.08 |
| nestJS MongoDB 연결 및 사용하기 (0) | 2024.11.25 |
| Proxy (프록시) (0) | 2024.11.06 |
| 자료구조와 자료형 (6) | 2024.10.09 |