개요
이번 시리즈에서는 기초부터 차근차근 시작하여, 10,000명의 동시 접속자가 발생하더라도 안정적으로 동작하는 실시간 채팅 서버를 구현해보려 합니다.
우선, 부하 테스트를 위한 기본적인 기능만 구현된 채팅 프로젝트에 대해 설명드리겠습니다.
Spring Security, JPA 등 기본적인 세팅은 되어 있다고 가정하고 진행하겠습니다.
주요 코드
build.gradle.kts
dependencies {
// WebSocket
implementation("org.springframework.boot:spring-boot-starter-websocket")
// ...생략
}
StompInterceptor
StompInterceptor를 통해 CONNECT 시 JWT Token을 받아 인증 정보를 처리합니다.
@Component
class StompInterceptor(
private val jwtTokenProvider: JwtTokenProvider
) : ChannelInterceptor {
override fun preSend(message: Message<*>, channel: MessageChannel): Message<*>? {
val accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor::class.java)
// CONNECT 메시지인 경우 토큰을 추출하여 인증 정보를 설정
if (accessor?.command == StompCommand.CONNECT) {
val headers = accessor.getNativeHeader("Authorization")
accessor.user = getAuthentication(headers)
}
return super.preSend(message, channel)
}
private fun getAuthentication(
headers: List<String>?
): Authentication {
val token = headers?.firstOrNull()
?: throw CustomException(ErrorCode.UNAUTHORIZED)
val jwtToken = token.substring(7)
return try {
jwtTokenProvider.getAuthentication(jwtToken)
} catch (e: Exception) {
throw CustomException(ErrorCode.UNAUTHORIZED)
}
}
}
WebSocketConfig
@Configuration
@EnableWebSocketMessageBroker
class WebSocketConfig(
private val stompInterceptor: StompInterceptor,
private val stompErrorHandler: StompErrorHandler
) : WebSocketMessageBrokerConfigurer {
override fun configureMessageBroker(registry: MessageBrokerRegistry) {
registry.enableSimpleBroker("/sub")
registry.setApplicationDestinationPrefixes("/pub")
}
override fun registerStompEndpoints(registry: StompEndpointRegistry) {
registry.addEndpoint("/ws-stomp")
.setAllowedOriginPatterns("*") // TODO : 배포 시 변경
.withSockJS()
registry.addEndpoint("/ws-stomp")
.setAllowedOriginPatterns("*") // TODO : 배포 시 변경
registry.setErrorHandler(stompErrorHandler)
}
override fun configureClientInboundChannel(registration: ChannelRegistration) {
registration.interceptors(stompInterceptor)
}
}
StompController
@RestController
class StompController(
private val simpMessagingTemplate: SimpMessagingTemplate,
private val chatService: ChatService,
private val authService: AuthService,
private val chatRoomNotificationService: ChatRoomNotificationService
) {
private val log = KotlinLogging.logger { }
@MessageMapping("/chat/{chatRoomId}")
fun chat(
@DestinationVariable chatRoomId: String,
request: MessageDto.Request,
authentication: Authentication
) {
// 닉네임 조회
val nickname = authService.getMyInfo(authentication.name.toLong()).nickname
// 채팅 저장
chatService.saveChat(
chatRoomId,
request,
authentication.name.toLong()
)
// 메시지 전송
simpMessagingTemplate.convertAndSend(
"/sub/${chatRoomId}",
MessageDto.Response.Message.of(nickname, request.message)
)
// 채팅방 실시간 갱신
chatRoomNotificationService.sendToClient(
chatRoomId,
request.message
)
}
}
결과
'Programming > SpringBoot' 카테고리의 다른 글
[10k-Chat] Spring Boot 실시간 채팅 서버 구현 (2) - WAS 이중화 (0) | 2025.02.08 |
---|---|
[SpringBoot] AMQP란? (0) | 2025.01.26 |
[SpringBoot] JPA 동적 스키마 (1) | 2024.11.06 |
[SpringBoot] @ModelAttribute 작동 원리 (0) | 2024.07.17 |
[SpringBoot] Entity의 ID를 테스트에서 삽입하는 방법 (0) | 2024.06.02 |