✍️ 기록
지우기 아까워서 남겨두는 기록 ...ㅎㅅㅎ....
이메일 전송과 관련된 기능을 작성하며 어느 글에서 SSE를 활용한 방식이라고 설명해 놓은 것을 보았다. 그래서 나는 이번에 알림 기능을 만들며 SSE가 아닌 웹소켓으로 알림 기능을 만들고 싶었고 웹소켓을 써보겠다고 하였다.
하나씩 예제들을 찾아보며 구현하던 도중 SSE와 내가 이메일을 구현해서 사용한 알림 보내기가 다른 것임을 알게 되었다. 그리고 일반적으로 알림기능은 서버에서 클라이언트로의 단방향 통신이 이루어져도 문제가 없는 기능이기에 양방향 통신인 웹소켓을 사용하여 복잡한 구현과 부하를 줄 필요가 없는 것 또한 깨닫게 되었다.
구글링으로 처음 알림 기능과 관련된 구현을 찾아봤을 때 "웹소켓을 활용한 알림 기능" 구현을 보게 되었고 다른 것을 찾아보지 않고 웹소켓을 활용한 알림 기능 구현들만 찾아보았다. 그래서 알림 기능을 웹소켓으로 대부분 구현한다고 생각하기도 했고 SSE 를 해봤기에 다른 기능을 구현하고싶다는 생각도 컸던 탓이다...ㅎㅎ
그러다 오늘 막히는 부분이 있어 github에서 다른 분들이 한 예제를 찾아보기 위해 "댓글 알림"과 같이 검색한 후 구현한 것을 봤는데 대부분이 SSE를 활용한 예제였고 이 또한 지금 내가 구현한 것과 크게 다르지 않음을 알아차리고는 내가 지금 하고 있는 게 SSE 구현인가... 웹소켓을 활용한 것인가 혼란이 오기 시작하였다.
이후 SSE 와 웹소켓 의 차이점에 대해 찾아보았고 알림 기능에는 SSE 구현이 적합하다는 것을 알게 되었다. 실시간 전송 과 클라이언트, 서버간의 통신이 중요한 게임과 채팅에서는 웹소켓을 사용! 간단하고 효율적인 구현, 필요할 때만 데이터를 받고 자동으로 재연결하고 서버에서 클라이언트로만의 단방향 소통만으로도 충분한 알림, 차트 기능에서는 SSE!
이 이외에도 내가 찾아 본 예제들에서 웹소켓을 활용한 기능들은 DB에 알림을 저장하지 않고 있었는데 그래서 또 궁금했다. 웹소켓을 활용하면 DB에 저장할 수 없는가? 그것은 아니었다. 그래서 그런 로직들이 들어가다보니 사실 github에서 찾아봤던 예제들과 구현이 비슷해 진 것이었다.
결론적으로 서버의 부하와 지속적인 관리의 복잡성을 덜기 위해 알림 기능에서는 SSE 를 활용하여 서비스를 구현하고자 하였다.
🔻어제까지 작성한 부분들
스프링에서는 웹소켓을 지원한다.
의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-websocket'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
메세지를 작성하기 위해서는 JSON 객체로 변경시켜 줘야하기 때문에 JSON 객체로 변환시켜주는 jackson 의존성도 추가해준다.
WebSocketConfig 클래스 파일 작성
서치를 해보는데 WebSocketConfigurer 종류가 여러 개라 어떤 것을 써야 할지 혼란이 왔다.
✨ WebSocketMessageBrokerConfigurer , WebSocketConfigurer , AbstractWebSocketMessageBrokerConfigurer ?
WebSocketMessageBrokerConfigurer | - WebSocket 메시지 브로커를 위한 설정 - STOMP 메시징( Simple / Stream Text Oriented Messaging Protocol 의 약자로 간단한 메세지)을 사용할 때 주로 사용 - 메시지 브로커와 엔드포인트 구성을 위해 사용 |
WebSocketConfigurer | - 웹소켓을 구성하기 위한 더 낮은 수준의 API - STOMP 레벨이 아닌 WebSocket 연결 자체에 대한 세부적인 설정 - WebSocket 엔드포인트 등록 |
AbstractWebSocketMessageBrokerConfigurer | - WebSocketMessageBrokerConfigurer의 구현을 제공하여 원하는 메서드만 오버라이드 가능 - Spring Framework 4.0.x 부터 4.2.x까지 주로 사용되었으며 이후 WebSocketMessageBrokerConfigurer 사용이 권장 |
각 인터페이스의 특징을 찾아본 후 WebSocketMessageBrokerConfigurer을 사용하였다.
그리고 WebSocketConfig 를 작성하기 전 WebSocketConfig 에 필요한 StompHandler를 작성해주었다. StompHandler는 웹소켓 연결 시 JWT 토큰을 검증하고, 사용자의 이메일을 기반으로 UsernamePasswordAuthenticationToken을 생성하여 SecurityContext에 설정하는 역할을 한다.
StompHandler 클래스
import com.codestates.server.global.security.auth.jwt.JwtTokenizer;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import lombok.RequiredArgsConstructor;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
@RequiredArgsConstructor
@Component
public class StompHandler implements ChannelInterceptor {
// JWT 토큰 처리
private final JwtTokenizer jwtTokenizer;
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
// STOMP 헤더 정보 추출
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
// 웹소켓 연결 요청 시 수행
if(StompCommand.CONNECT.equals(accessor.getCommand())) {
// Jwt 토큰 추출
String token = accessor.getFirstNativeHeader("Authorization");
try {
// 토큰 검증 및 클레임 추출
Jws<Claims> claims = jwtTokenizer.getClaims(token, jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey()));
// 사용자 메일 추출
String email = claims.getBody().getSubject();
// SecurityContext 에 사용자 정보 설정
// 사용자 이메일을 기반으로 Authentication 객체 생성
// -> 굳이 credential 정보 authorities 정보 없어도 됨
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(email, null, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
} catch (Exception e) {
// 검증 실패시 예외발생
throw new IllegalArgumentException("Invalid Token");
}
}
return message;
}
}
WebSocketConfig 클래스
WebSocketConfig 설정에 StompHandler를 추가하여 웹소켓 연결 시 사용자 인증을 처리
import com.codestates.server.global.alarm.websocket.handler.StompHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private final StompHandler stompHandler;
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws-stomp").setAllowedOriginPatterns("*").withSockJS();
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(stompHandler);
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic", "/queue");
registry.setApplicationDestinationPrefixes("/app");
}
}
AlarmService
AlarmService 사용자에게 알림을 전송하는 로직 구현
package com.codestates.server.global.alarm.service;
import com.codestates.server.global.alarm.entity.Alarm;
import com.codestates.server.global.alarm.repository.AlarmRepository;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@AllArgsConstructor
public class AlarmService {
private final AlarmRepository alarmRepository;
public Alarm createAlarm(Alarm alarm) {
return alarmRepository.save(alarm);
}
}
그동안 참고했던.. 🥲
📖 참고자료
'PROJECT' 카테고리의 다른 글
[PROJECT] 프로젝트 ver.2 업그레이드 시키기 (1) | 2023.10.30 |
---|---|
[PROJECT] 사용자 역할 부여, 북마크 조회 시 회원 정보 무시 - 코드 오류 수정 (0) | 2023.10.23 |
[PROJECT] pre-project stackoveflow 클론 코딩 / WEEK2 기록 (0) | 2023.10.05 |
[PROJECT] 메인프로젝트 회고 / 기록 남기기 (1) | 2023.09.24 |
[PROJECT] Main 프로젝트 기록 / WEEK1 기록 및 회고 (1) | 2023.08.30 |