실시간 통신이 필요한 웹 애플리케이션에서 WebSocket은 필수적인 기술입니다. 이 글에서는 AWS Lambda와 API Gateway를 활용하여 확장성 있는 서버리스 WebSocket 서비스를 구현하는 방법을 자세히 알아보겠습니다.
전체 아키텍처 개요
이 서비스는 다음과 같은 AWS 서비스로 구성됩니다:
- API Gateway - WebSocket 연결을 관리하는 엔드포인트
- Lambda 함수 - 연결, 연결 해제, 메시지 전송을 처리
- DynamoDB - 클라이언트 연결 정보를 저장
세 개의 Lambda 함수가 각각 다른 WebSocket 이벤트를 처리합니다:
- onConnect.py - 클라이언트 연결 처리
- onDisconnect.py - 클라이언트 연결 해제 처리
- sendMessage.py - 클라이언트 간 메시지 전송 처리
클라이언트 연결 처리 (onConnect)
def lambda_handler(event, context):
connection_id = event.get('requestContext',{}).get('connectionId')
query_params = event.get('queryStringParameters', {}) or {}
room_uuid = query_params.get('uuid')
if not room_uuid and event.get('body'):
try:
body = json.loads(event.get('body'))
room_uuid = body.get('uuid')
except (json.JSONDecodeError, TypeError):
pass
연결 핸들러는 다음과 같은 작업을 수행합니다.
- 클라이언트의 연결 ID를 추출합니다.
- 쿼리 파라미터나 요청 본문에서 UUID를 추출합니다.
- UUID가 없거나 유효하지 않으면 오류를 반환합니다.
- 유효한 UUID인 경우, 연결 ID와 UUID를 DynamoDB 테이블에 저장합니다.
UUID는 "룸" 또는 "채널" 개념으로 사용되어, 특정 그룹에 메시지를 전송할 때 활용됩니다. 이는 채팅방이나 게임 세션과 같은 사용 사례에 적합합니다.
클라이언트 연결 해제 처리 (onDisconnect)
def lambda_handler(event, context):
connection_id = event.get('requestContext',{}).get('connectionId')
try:
connection_data = connections.get_item(Key={'id': connection_id})
if 'Item' in connection_data:
uuid = connection_data['Item'].get('uuid')
logger.info(f"연결 해제: ID={connection_id}, UUID={uuid}")
except Exception as e:
logger.warning(f"연결 정보 조회 중 오류: {str(e)}")
connections.delete_item(Key={'id': connection_id})
연결 해제 핸들러는 다음과 같은 작업을 수행합니다.
- 연결 해제된 클라이언트의 연결 ID를 추출합니다.
- DynamoDB에서 해당 연결의 정보를 조회하여 로깅합니다.
- DynamoDB에서 해당 연결 정보를 삭제합니다.
이 과정을 통해 더 이상 활성화되지 않은 연결을 데이터베이스에서 제거하여 리소스를 효율적으로 관리합니다.
메시지 전송 처리 (sendMessage)
def lambda_handler(event, context):
request_body = json.loads(event.get('body', '{}'))
post_data = request_body.get('data')
target_uuid = request_body.get('uuid')
domain_name = event.get('requestContext',{}).get('domainName')
stage = event.get('requestContext',{}).get('stage')
current_connection_id = event.get('requestContext',{}).get('connectionId')
endpoint_url = f"https://{domain_name}/{stage}"
if target_uuid:
response = connections.scan(
FilterExpression=Attr('uuid').eq(target_uuid)
)
else:
items = connections.scan(ProjectionExpression='id,uuid').get('Items', [])
메시지 전송 핸들러는 더 복잡한 작업을 수행합니다.
1.클라이언트로부터 받은 메시지 데이터와 대상 UUID를 추출합니다.
2.API Gateway 엔드포인트 URL을 구성합니다.
3.두 가지 모드로 작동합니다.
특정 UUID가 지정된 경우: 해당 UUID에 연결된 클라이언트에만 메시지를 전송
UUID가 지정되지 않은 경우: 모든 연결된 클라이언트에 브로드캐스트
4.메시지에 발신자 UUID 정보를 추가합니다.
5.연결된 모든 대상 클라이언트에 메시지를 전송합니다.
6.종료된 연결을 감지하여 DynamoDB에서 자동으로 정리합니다.
구현의 주요 특징
1. UUID 기반 룸 시스템
이 구현의 핵심은 UUID를 사용한 "룸" 개념입니다. 클라이언트는 연결 시 UUID를 제공해야 하며, 이 UUID는 클라이언트 그룹을 구분하는 데 사용됩니다.
- 특정 그룹에만 메시지 전송 가능
- 다중 채팅방, 게임 세션 등 구현 가능
- 다양한 애플리케이션 요구사항에 맞게 확장 가능
2. 자동 연결 관리
연결 해제 시 자동으로 DynamoDB에서 데이터가 삭제됩니다. 또한 메시지 전송 시 존재하지 않는 연결이 감지되면 자동으로 정리하는 기능이 포함되어 있습니다.
- 오래된 연결 정보로 인한 리소스 낭비 방지
- 메시지 전송 실패 시 연결 상태 자동 갱신
- 시스템 안정성과 성능 최적화
3. 상세한 로깅 시스템
모든 Lambda 함수에는 상세한 로깅이 포함되어 있어 문제 해결과 모니터링이 용이합니다.
- 연결/연결 해제 이벤트 로깅
- 메시지 전송 성공/실패 로깅
- 오류 상황에 대한 자세한 정보 기록
실제 활용 사례
이 WebSocket 구현은 다음과 같은 애플리케이션에 적합합니다.
- 실시간 채팅 서비스 - UUID를 채팅방 ID로 활용
- 멀티플레이어 게임 - 게임 세션별로 UUID 할당
- 실시간 협업 도구 - 문서나 프로젝트별로 UUID 할당
- IoT 애플리케이션 - 장치 그룹별로 UUID 할당하여 제어 신호 전송
확장 방안
이 구현은 다음과 같이 확장할 수 있습니다.
- 사용자 인증 추가 - JWT 토큰이나 Cognito를 활용한 사용자 인증
- 메시지 영속성 - 중요 메시지를 DynamoDB에 저장하여 오프라인 클라이언트를 위한 히스토리 제공
- 메시지 필터링 - 특정 조건에 따라 메시지를 필터링하거나 변환
- 웹훅 통합 - 특정 이벤트 발생 시 외부 서비스 호출
결론
AWS Lambda, API Gateway, DynamoDB를 활용한 이 WebSocket 구현은 서버리스 아키텍처의 장점을 최대한 활용합니다. 확장성, 유지보수 용이성, 비용 효율성을 모두 갖춘 이 구현은 다양한 실시간 통신 요구사항을 충족할 수 있습니다.
특히 UUID를 활용한 룸 시스템은 다양한 사용 사례에 맞게 유연하게 적용할 수 있으며, 자동화된 연결 관리 시스템은 서비스의 안정성을 향상시킵니다.
댓글