팀 프로젝트를 진행하며 Spring Boot로 아주 단순한 1:1 채팅을 구현했다.
이번에 NestJS로는 1:1, 단체 채팅, 메시지 읽음 처리 등 좀 더 그럴듯한(ㅋㅋㅋ) 채팅을 만들어 보았다.
크게 세 가지 부분에서 NestJS가 Socket 통신에 더 적합하다고 느꼈다.
- 자동설정: Config 클래스를 작성하지 않아도 된다.
- @WebSocketGateway 클래스 구조의 간결성
- Gateway DI의 편리함
자세히 살펴보자😆
기본적으로 NestJS는 @WebSocketGateway 클래스로 웹소켓 설정과 이벤트 핸들러를 등록한다.
그러나 Spring Boot에서는 Config 클래스를 작성하고, 또 다른 클래스에서 이벤트 핸들러를 작성해야 한다.
적당한 자동설정을 통해 코드의 양이 획기적으로 줄어든다는 점에서 NestJS의 승리!
Spring Boot 사용 시 WebSocketConfig 클래스를 작성해줘야 한다.
설정 값은 코드의 주석으로 대신한다.
@Configuration
@EnableWebSocketMessageBroker // WebSocket, STOMP 활성화
public class StompWebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/stomp/chat") // 클라이언트가 WebSocket 연결할 엔드포엔트 등록
.setAllowedOrigins("...생략")
.withSockJS();
}
// 어플리케이션 내부에서 사용할 path 지정
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// Client에서 SEND 요청을 처리
registry.setApplicationDestinationPrefixes("/pub");
// /sub 주제를 구독하는 클라이언트에게 메시지 전송
registry.enableSimpleBroker("/sub");
}
}
반면 NestJS는 Gateway 클래스에 설정과 이벤트 핸들러를 모두 작성할 수 있다.
필요한 설정값은 @WebSocketGateway 데코레이터의 메타데이터로 넘겨준다.
그리고 이 클래스에 이벤트 핸들러를 적으면 별도의 코드 작성 없이 이벤트 핸들러가 등록된다.
@WebSocketGateway({
namespace: "chat", // 네임스페이스 설정
cors: {
origin: ["http://localhost:3000"]
}
})
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer()
server: Server;
private chatClients: Set<string> = new Set();
constructor(private readonly chatService: ChatService) {
}
handleConnection(socket: Socket) {
// 이하 생략
. | Spring Boot | Nest JS |
---|---|---|
WebSocket 서버 초기화 | 설정 클래스를 작성해야 WebSocket 핸들러, 엔드포인트 등을 정의하고 WebSocket 서버 초기화 | @WebSocketGateway 데코레이터로 초기화 가능. 즉, NestJS가 내부적으로 소켓 서버 초기화 및 설정 |
핸들러 등록 | WebSocket 핸들러 정의하고 config 클래스에 등록해야 함. 혹은 @MessageMapping 어노테이션을 통해 핸들러를 작성하고, config에서 등록 | @WebSocketGateway 클래스 내에서 WebSocket 핸들러를 정의, WebSocket 서버에 자동 등록 개발자는 별도의 등록 코드를 작성할 필요X |
루트 URL 지정 | WebSocket 핸들러에 매핑할 루트 URL을 정의해야함. 클라이언트는 이를 통해 WebSocket 연결을 요청 | @WebSocketGateway로 루트 URL 지정 가능 |
앞서 언급했듯, Spring Boot 환경에선 이벤트 핸들러가 (분리된) 클래스에 작성된다.
이때, 이벤트 핸들러를 작성하는 방법엔 크게 두 가지가 있다.
- WebSocket 핸들러 클래스 구현하기
- @MessageMapping 어노테이션으로 (이미 존재하는) 컨트롤러에 메소드 추가
이 중 나는 2번 방법으로 채팅을 구현했었다.
Spring Boot는 @MessageMapping을 통해, NestJS는 @SubscribeMessage를 통해 이벤트 핸들러를 작성한다.
@SubscribeMessage는 WebSocketGateway 클래스 안에, @MessageMapping은 이미 존재하는 컨트롤러 안에 작성한다.
즉, @MessageMapping은 HTTP 메소드와 함께 있고, @SubscribeMessage는 이벤트 핸들러끼리 모여 있다.
나는 WebSocketGateway 방식의 작성법이 더 명확하다고 생각한다.
컨트롤러 안에 HTTP 메소드와 채팅 이벤트 핸들러가 혼재하는 것 보다는,
이벤트 로직은 이벤트 로직끼리 있는 것이 간결해보인다.
(은근 이벤트 핸들러 보러 컨트롤러 가는 것도 귀찮다 🤣)
네가 WebSocket 핸들러 클래스를 만들고 이 클래스를 이벤트 핸들러로 등록했으면 되는 일 아니냐?
맞아요🤣
그런데 너무 간단한 채팅 구현이라 핸들러 메소드가 하나밖에 없었다.
그렇다고 치더라도 Config 클래스를 만들고 컨트롤러에 이벤트 핸들러를 두는 것보다,
Gateway 하나에 소켓 관련 코드가 모여있는 게 직관적이라고 느낀다.
이 클래스 자체를 DI 할 수 있다는 점에서 Gateway 방식의 편리함을 또 한번 느낄 수 있다.
private async createComment(commentId: number, options: Partial<Comment>, boardUserId: number): Promise<CommentResponseDto> {
// 댓글 저장 로직은 생략
// 댓글 작성 시 글 주인에게 socket 알림 ⭐️⭐️⭐️
this.alarmGateway.handleCommentCreated(new CommentCreatedAlarm(comment), boardUserId);
return new CommentResponseDto(comment);
}
이처럼 유저 A가 B의 게시글에 댓글을 달았을 때, B에게 실시간 알림을 보내는 로직을
AlarmGateway 클래스를 DI해서 재사용할 수 있다.