서론
해당하는 프로젝트를 시작하기에 앞서 이전까지는 웹 소켓을 이용해 채팅 기능, 사용자 위치 갱신 기능 등 다양한 기능을 구현하였다. 그러나, 새로운 프로젝트를 진행하던 중 웹 소켓 하나의 기능을 위해 별도의 EC2 또는 ECS 인스턴스를 실행할 경우 비용이 증가할 것으로 판단하였다. 이에 따라, 웹 소켓 기능만 서버리스로 구현한다면 트래픽이 발생하는 경우에만 비용이 발생하므로, 효율적인 비용 관리가 가능할 것이라고 생각하였다. 따라서, 웹 소켓을 어떻게 서버리스로 구현할 수 있는지에 대해 탐구하게 되었다.
WebSocket
웹 소켓은 MDN 페이지에 설명된 것처럼, 지속적으로 클라이언트와 서버간의 통신을 가능하게 하는 프로토콜이다.클라이언트가 서버에 연결하면, 해당 연결은 유지되며 서버는 원하는 때에 클라이언트에게 응답할 수 있다. 이렇게 함으로써, REST API 만으로는 부족한 실시간 통신을 보장할 수 있게 된다. 웹 소켓은 대표적으로 실시간 사이트 갱신, 채팅 기능을 구현할 때 사용한다.
Serverless Websocket
서버리스(serverless)란 개발자가 서버를 관리할 필요 없이 애플리케이션을 빌드하고 실행할 수 있도록 하는 클라우드 네이티브 개발 모델이다.
AWS에서 구현하는 Serverless WebSocket은 어떤 아키텍처로 구성되는것일까?
Severless Websocket은 Serverless REST API를 구현하는 것과 동일한 구조인 API Gateway와 Lambda Function 을 사용한다. Serverless WebSocket은 AWS의 서버리스 서비스인 Lambda를 바탕으로 구현되지만, 일반적인 Lambda 와는 다르게, API Gateway를 구현할 때부터 WebSocket 기능을 연결해야 한다.
Amazon API Gateway
Amazon API Gateway는 모든 규모에서 REST, HTTP 및 WebSocket API를 생성, 게시, 유지 관리, 모니터링 및 보호하기 위한 AWS 서비스이다.
API Gateway는 클라이언트와 AWS 리소스간의 연결 역할을 한다. 일반적으로 REST API, HTTP API와 AWS 리소스를 연결하기 위해 사용하지만, 이번에는 WebSocket 기능을 구현하기 위해 API Gateway를 사용할 것이다.
AWS Lambda
AWS Lambda는 서버를 프로비저닝하거나 관리하지 않고도 코드를 실행할 수 있는 컴퓨팅 서비스이다.
일반적인 REST API를 사용하는 Lambda의 경우 각 함수에 진입하기 위한 핸들러가 존재하는데, WebSocket API의 경우 3가지의 핸들러가 존재한다.
- $connect: 클라이언트가 WebSocket을 최초로 연결하였을 때 실행되는 핸들러
- $disconnect: 클라이언트가 WebSocket을 종료하였을 때 실행되는 핸들러
- $default: 이외의 상황에서 WebSocket을 호출하였을 때 실행되는 핸들러
3가지의 핸들러 이외에도 개발자는 특정 파라미터를 바탕으로 여러 개의 커스텀 핸들러를 구현하여 라우팅할 수 있다.
하나의 예시 코드를 바탕으로 WebSocket API를 확인해보자.
import { APIGatewayEvent } from 'aws-lambda';
import { ApiGatewayManagementApi, PostToConnectionRequest } from "@aws-sdk/client-apigatewaymanagementapi";
// $connect 핸들러
export async function handleConnect(event: APIGatewayEvent): Promise<{ statusCode: number }> {
return { statusCode: 200 };
}
// $default 핸들러
export async function handleMessage(event: APIGatewayEvent): Promise<{ statusCode: number }> {
const connectionId = event.requestContext.connectionId; // (1)
const body = event.body; // (2)
if (!body) return { statusCode: 200 };
const managementApi: ApiGatewayManagementApi = new ApiGatewayManagementApi({
endpoint: `${event.requestContext.domainName}/${event.requestContext.stage}`,
}); // (3)
await managementApi.postToConnection({
Data: parseConnectionRequestData(body),
ConnectionId: connectionId,
}); // (4)
return { statusCode: 200 }; // (5)
}
type PostToConnectionRequestData = PostToConnectionRequest['Data'];
// $default 핸들러에서 사용할 데이터를 파싱
function parseConnectionRequestData(body: string): PostToConnectionRequestData {
return new TextEncoder().encode(body);
}
$connect 핸들러
웹 소켓이 서버에 연결을 요청하였을 때 가장 먼저 호출되는 핸들러이다. 만약 $connect 핸들러를 구현하지 않고 WebSocket API를 구현하게 될 경우 ‘500 Server Internal Error’가 발생하여 서버에 접속할 수 없게 된다. 따라서 실질적인 역할을 하지 않더라도, 핸들러는 연결 성공을 알리는 200 statusCode를 전달해야한다.
$default 핸들러
웹 소켓이 서버에 연결을 성공한 후 통신하기 위해 사용하는 핸들러이다. 현재는 모든 요청사항이 handleMessage 함수를 사용하도록 구현하였지만, 별도의 커스텀 핸들러를 구현하여 여러개의 함수를 통해 라우팅될 수 있도록 구현할 수 있다.
handleMessage 함수는 아래와 같은 비즈니스 로직을 수행하게 된다.
- 클라이언트의 WebSocket Id를 조회한다.
- 클라이언트의 Body 데이터가 존재하지 않는다면, 다음 비즈니스 로직을 수행하지 않고, 요청을 종료한다.
- 클라이언트가 요청한 ApiGateway와 연결하기 위해, ApiGatewayManagementApi를 선언한다.
- $default 핸들러에 요청한 클라이언트의 주소로, body에 전달된 데이터를 Echo한다.
- 성공적으로 비즈니스 로직이 수행되었으므로 statusCode를 200으로 전달하여 요청을 종료한다.
예시 코드에서는 특이하게 parseConnectionRequestData 함수가 존재하는데, aws-sdk-js가 v3로 업데이트 되면서 postToConnection 메서드를 사용할 때, String 형식을 지원하지 않아 인코딩을 하기 위해 구현하였다.
$disconnect 핸들러
웹 소켓이 서버에 연결을 종료한 후 사용하는 핸들러이다. 현재는 구현되어 있지 않지만, 연결되어 있는 사용자를 관리하는 변수 또는 데이터베이스에 특정 클라이언트에서 특정 클라이언트의 정보를 삭제하기 위해 구현할 수 있다.
'분석과 탐구' 카테고리의 다른 글
제로부터 시작하는 Prisma와 Nest.js (0) | 2023.07.16 |
---|---|
아키텍처 패턴 그리고 헥사고날 아키텍처 (0) | 2023.07.02 |
제로부터 시작하는 DDD를 위한 이벤트스토밍 (0) | 2023.06.18 |
1일 1커밋에서 벗어나기 (0) | 2023.05.07 |
"RxJS" 넌 도대체 뭐니? (0) | 2023.02.25 |