Skip to content

Conversation

@bs8841
Copy link
Collaborator

@bs8841 bs8841 commented Sep 17, 2025

요구사항

기본

웹소켓 구현하기

  • 웹소켓 환경 구성
    spring-boot-starter-websocket 의존성을 추가하세요.

implementation 'org.springframework.boot:spring-boot-starter-websocket'
웹소켓 메시지 브로커 설정

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {...}

메모리 기반 SimpleBroker를 사용하세요.

@Override
public void configureMessageBroker(MessageBrokerRegistry config) {...}

SimpleBroker의 Destination Prefix는 /sub 으로 설정하세요.
클라이언트에서 메시지를 구독할 때 사용합니다.
Application Destination Prefix는 /pub 으로 설정하세요.
클라이언트에서 메시지를 발행할 때 사용합니다.

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {...}

STOMP 엔드포인트는 /ws로 설정하고, SockJS 연결을 지원해야 합니다.

  • 메시지 송신
    첨부파일이 없는 단순 텍스트 메시지인 경우 STOMP를 통해 메시지를 전송할 수 있도록 컨트롤러를 구현하세요.
@Controller
public class MessageWebSocketController {
    ...
    @MessageMapping(...)
}

클라이언트는 웹소켓으로 /pub/messages 엔드포인트에 메시지를 전송할 수 있어야 합니다.
@MessageMapping을 활용하세요.
메시지 전송 요청의 페이로드 타입은 MessageCreateRequest 를 그대로 활용합니다.
첨부파일이 포함된 메시지는 기존의 API (POST /api/messages)를 그대로 활용합니다.

  • 메시지 수신
    클라이언트는 채널 입장 시 웹소켓으로 /sub/channels.{channelId}.messages 를 구독해 메시지를 수신합니다.

이를 고려해 메시지가 생성되면 해당 엔드포인트로 메시지를 보내는 컴포넌트를 구현하세요.

@Component
public class WebSocketRequiredEventListener {
    ...
    private final SimpMessagingTemplate messagingTemplate;
    
  @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
  public void handleMessage(MessageCreatedEvent event) {...}
}

MessageCreatedEvent를 통해 새로운 메시지 생성 이벤트를 확인하세요.
SimpMessagingTemplate를 통해 적절한 엔드포인트로 메시지를 전송하세요.

SSE 구현하기

  • SSE 환경을 구성하세요.

    • 클라이언트에서 SSE 연결을 위한 엔드포인트를 구현하세요.

      • GET /api/sse
    • 사용자별 SseEmitter 객체를 생성하고 메시지를 전송하는 컴포넌트를 구현하세요.

      @Service public class SseService {

      public SseEmitter connect(UUID receiverId, UUID lastEventId) {...}

      public void send(Collection<UUID> receiverIds, String eventName, Object data) {...}

      public void broadcast(String eventName, Object data) {...}

      @Scheduled(fixedDelay = 1000 * 60 * 30)
      public void cleanUp() {...}

      private boolean ping(SseEmitter sseEmitter) {...}
      }

      • connect: SseEmitter 객체를 생성합니다.
      • send, broadcast: SseEmitter 객체를 통해 이벤트를 전송합니다.
      • cleanUp: 주기적으로 ping을 보내서 만료된 SseEmitter 객체를 삭제합니다.
      • ping: 최초 연결 또는 만료 여부를 확인하기 위한 용도로 더미 이벤트를 보냅니다.
    • SseEmitter 객체를 메모리에서 저장하는 컴포넌트를 구현하세요.

      @Repository
      public class SseEmitterRepository {
      private final ConcurrentMap<UUID, List<SseEmitter>> data = new ConcurrentHashMap<>();
      ...
      }
      • ConcurrentMap: 스레드 세이프한 자료구조를 사용합니다.
      • List<SseEmitter>: 사용자 당 N개의 연결을 허용할 수 있도록 합니다. (예: 다중 탭)
    • 이벤트 유실 복원을 위해 SSE 메시지를 저장하는 컴포넌트를 구현하세요.

      @Repository
      public class SseMessageRepository {

      private final ConcurrentLinkedDeque<UUID> eventIdQueue = new ConcurrentLinkedDeque<>();
      private final Map<UUID, SseMessage> messages = new ConcurrentHashMap<>();
      ...
      }

      • 각 메시지 별로 고유한 ID를 부여합니다.
      • 클라이언트에서 LastEventId를 전송해 이벤트 유실 복원이 가능하도록 해야 합니다.
  • 기존에 클라이언트에서 폴링 방식으로 주기적으로 요청하던 데이터를 SSE를 이용해 서버에서 실시간으로 전달하는 방식으로 리팩토링하세요.

    • 새로운 알림 이벤트 전송

      • 새 알림이 생성되었을 때 클라이언트에 이벤트를 전송하세요.

      • 클라이언트는 이 이벤트를 수신하면 알림 목록에 알림을 추가합니다.

      • 이벤트 명세

        id 이벤트 고유 ID
        name notifications.created
        data NotificationDto

배포 아키텍처 구성하기

  • 다음의 다이어그램에 부합하는 배포 아키텍처를 Docker Compose를 통해 구현하세요.
image

심화

웹소켓 인증/인가 처리하기

  • 인증 처리

디스코드잇 클라이언트는 CONNECT 프레임의 헤더에 다음과 같이 Authorization 토큰을 포함합니다.

CONNECT
Authorization:Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ3b29keSIsImV4cCI6MTc0OTM5MzA0OCwiaWF0IjoxNzQ5MzkyNDQ4LCJ1c2VyRHRvIjp7ImlkIjoiMDQwZTk2ZWMtMjdmNC00Y2MxLWI4MWQtNTMyM2ExZWQ5NTZhIiwidXNlcm5hbWUiOiJ3b29keSIsImVtYWlsIjoid29vZHlAZGlzY29kZWl0LmNvbSIsInByb2ZpbGUiOm51bGwsIm9ubGluZSI6bnVsbCwicm9sZSI6IlVTRVIifX0.JOkvCpnR0e0KMQYLh_hUWglgTvUIlfQOT58eD4Cym5o
accept-version:1.2,1.1,1.0
heart-beat:4000,4000
서버 측에서는 ChannelInterceptor를 구현하여 연결 시 토큰을 검증하고, 인증된 사용자 정보를 SecurityContext에 설정해야 합니다.

참고 문서: Spring 공식 문서

@Configuration
@EnableWebSocketMessageBroker
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    ...
  @Override
  public void configureClientInboundChannel(ChannelRegistration registration) {
    registration.interceptors(...);
  }
}

CONNECT 프레임일 때 엑세스 토큰을 검증하는 JwtAuthenticationChannelInterceptor 구현체를 정의하세요.

public class JwtAuthenticationChannelInterceptor implements ChannelInterceptor {
    
    @Override
  public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message,
        StompHeaderAccessor.class);
      if (StompCommand.CONNECT.equals(accessor.getCommand())) {
          ... // 검증 로직
          
          UsernamePasswordAuthenticationToken authentication = ...
          accessor.setUser(authentication);
      }
      return message;
  }
  
}

검증 로직은 이전에 구현한 JwtAuthenticationFilter를 참고하세요.
인증이 완료되면 SecurityContext에 인증정보를 저장하는 대신 accessor 객체에 저장하세요.
SecurityContextChannelInterceptor를 등록하여 이후 메시지 처리 흐름에서도 인증 정보를 활용할 수 있도록 구성하세요.

  @Override
  public void configureClientInboundChannel(ChannelRegistration registration) {
    registration.interceptors(
        jwtAuthenticationChannelInterceptor,
      new SecurityContextChannelInterceptor(),
    );
  }
  • 인가 처리

AuthorizationChannelInterceptor를 사용해 메시지 권한 검사를 수행합니다.

AuthorizationChannelInterceptor를 활용하기 위해의존성을 추가하세요.

implementation 'org.springframework.security:spring-security-messaging'
MessageMatcherDelegatingAuthorizationManager를 활용해 인가 정책을 정의하고, 채널에 추가하세요.

  private AuthorizationChannelInterceptor authorizationChannelInterceptor() {
    return new AuthorizationChannelInterceptor(
        MessageMatcherDelegatingAuthorizationManager.builder()
            .anyMessage().hasRole(Role.USER.name())
            .build()
    );
  }
  @Override
  public void configureClientInboundChannel(ChannelRegistration registration) {
    registration.interceptors(
        jwtAuthenticationChannelInterceptor,
      new SecurityContextChannelInterceptor(),
        authorizationChannelInterceptor()
    );
  }

분산 환경 배포 아키텍처 구성하기

  • 다음의 다이어그램에 부합하는 배포 아키텍처를 Docker Compose를 통해 구현하세요.
image

주요 변경사항

스크린샷

image

멘토에게

  • 셀프 코드 리뷰를 통해 질문 이어가겠습니다.

@bs8841 bs8841 requested a review from codingjigi September 17, 2025 14:57
@bs8841 bs8841 self-assigned this Sep 17, 2025
@bs8841 bs8841 added 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. 미완성🛠️ 스프린트 미션 제출일이지만 미완성했습니다. 죄송합니다. 지각제출⏰ 제출일 이후에 늦게 제출한 PR입니다. labels Sep 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. 미완성🛠️ 스프린트 미션 제출일이지만 미완성했습니다. 죄송합니다. 지각제출⏰ 제출일 이후에 늦게 제출한 PR입니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant