여러분, 안녕하세요! 오늘은 우리 일상에서 자주 접하는 실시간 알림 시스템을 Spring Boot와 Server-Sent Events(SSE)를 활용해 직접 구현해보려고 합니다.
넷플릭스에서 새 시리즈가 출시되었을 때 알림을 받거나, 쇼핑몰에서 관심 상품이 할인될 때 바로 소식을 받는 그 기술 말이죠.
이런 실시간 알림은 현대 웹 애플리케이션에서 사용자 경험을 크게 향상시키는 핵심 요소입니다.
SSE는 서버에서 클라이언트로 단방향 통신을 제공하는 가벼운 프로토콜로, 실시간 알림 구현에 아주 적합합니다. 웹소켓보다 가볍고 HTTP 프로토콜 위에서 동작하기 때문에 구현이 상대적으로 간단하죠. 자, 이제 직접 코드를 작성하며 실시간 알림 시스템을 만들어봅시다!
SSE(Server-Sent Events)는 서버에서 클라이언트로 데이터를 지속적으로 스트리밍하는 기술입니다.
웹소켓과 달리 단방향 통신만 지원하며, HTTP 프로토콜을 기반으로 합니다.
마치 서버가 클라이언트에게 "새로운 소식이 있어요, 들어보세요!"라고 속삭이는 것과 같죠.
실시간 알림, 라이브 업데이트, 모니터링 대시보드 등에 적합한 기술입니다.
특히 양방향 통신이 필요 없고 서버에서 클라이언트로 데이터를 푸시하는 경우에 웹소켓보다 가볍고 구현이 쉽다는 장점이 있어요.
먼저 Spring Boot 프로젝트를 생성해 봅시다.
Spring Initializer를 사용하거나 IDE에서 직접 생성할 수 있습니다.
이 과정에서 필요한 의존성은 다음과 같습니다.
pom.xml 파일에서 웹 의존성을 Spring Reactive Web으로 변경해줍니다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
이제 서버 측 코드를 작성해 봅시다.
먼저 알림을 표현할 데이터 모델을 만들고, 그 다음 알림을 처리할 컨트롤러를 구현하겠습니다.
알림을 표현하기 위한 Notification 클래스를 정의합니다. 메시지 내용과 타임스탬프를 포함합니다.
package com.study.nofitication
import java.time.Instant
data class Notification(
val message: String,
val timestamp: Long = Instant.now().toEpochMilli()
) {
// 기본 생성자
constructor(message: String) : this(message, Instant.now().toEpochMilli())
// Getter 메서드들
fun getMessage(): String {
return message
}
fun getTimestamp(): Long {
return timestamp
}
}
여기서는 Kotlin의 data class를 사용하여 간결하게 정의했습니다.
메시지와 타임스탬프 두 가지 속성을 갖고 있으며, 타임스탬프는 기본적으로 현재 시간을 밀리초 단위로 설정합니다.
또한 메시지만 받는 생성자를 추가하여 사용의 편의성을 높였습니다.
Getter 메서드는 JSON 직렬화를 위해 명시적으로 추가했습니다.
이는 Java 스타일의 getter 메서드를 제공함으로써 Spring의 Jackson 라이브러리가 객체를 JSON으로 변환할 때 필요합니다.
이제 알림을 발행하고 구독할 수 있는 컨트롤러를 만들어 봅시다.
package com.study.nofitication
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import reactor.core.publisher.Flux
import reactor.core.publisher.Sinks
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.CrossOrigin
@RestController
@RequestMapping("/notifications")
@CrossOrigin // CORS 허용을 위한 어노테이션
class NotificationController {
// 다중 구독자에게 알림을 배포하기 위한 Sink 생성
private val notificationsSink = Sinks.many().multicast().onBackpressureBuffer<Notification>()
/**
* 알림 메시지를 발행하는 API
* @param message 발행할 알림 메시지
* @return 발행 결과 메시지
*/
@GetMapping("/send")
fun publishNotification(@RequestParam message: String): String {
val notification = Notification(message)
notificationsSink.tryEmitNext(notification)
return "Notification sent: $message"
}
/**
* 알림 스트림을 제공하는 API
* TEXT_EVENT_STREAM_VALUE 미디어 타입을 사용하여 SSE 스트림 형태로 응답
* @return 알림 Flux 스트림
*/
@GetMapping(produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
fun streamNotifications(): Flux<Notification> {
return notificationsSink.asFlux()
}
}
이 컨트롤러는 두 가지 핵심 API를 제공합니다:
핵심은 Sinks.many().multicast().onBackpressureBuffer()입니다.
이는 Spring WebFlux에서 제공하는 반응형 스트림 처리를 위한 싱크로, 다중 구독자에게 이벤트를 배포하는 역할을 합니다.
마치 한 명의 뉴스 앵커가 여러 시청자에게 뉴스를 전달하는 것과 같은 구조죠.
publishNotification 메서드는 메시지를 받아 새 알림을 만들고 싱크에 발행합니다.
그리고 streamNotifications 메서드는 싱크를 Flux로 변환하여 SSE 스트림으로 제공합니다. MediaType.TEXT_EVENT_STREAM_VALUE는 HTTP 응답 헤더에 Content-Type: text/event-stream을 추가하여 브라우저가 이를 이벤트 스트림으로 인식하게 합니다.
이제 클라이언트 측 코드를 작성해 봅시다.
HTML과 JavaScript를 사용하여 서버에서 스트리밍되는 알림을 구독하고 화면에 표시할 것입니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SSE 실시간 알림 테스트</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
h1 {
color: #333;
text-align: center;
margin-bottom: 30px;
}
#notifications {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 15px;
list-style-type: none;
}
#notifications li {
padding: 10px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
}
#notifications li:last-child {
border-bottom: none;
}
.message {
flex-grow: 1;
}
.timestamp {
color: #777;
font-size: 0.8em;
margin-left: 15px;
}
.connection-status {
text-align: center;
margin-bottom: 15px;
font-weight: bold;
}
.connected {
color: green;
}
.disconnected {
color: red;
}
</style>
</head>
<body>
<h1>SSE 실시간 알림 테스트</h1>
<div id="status" class="connection-status disconnected">연결 대기 중...</div>
<ul id="notifications"></ul>
<script>
const statusElement = document.getElementById('status');
const notificationsElement = document.getElementById('notifications');
// EventSource 객체 생성 - SSE 연결
const eventSource = new EventSource("http://localhost:8080/notifications");
// 연결이 열릴 때
eventSource.onopen = () => {
console.log("SSE 연결 성공");
statusElement.textContent = "서버에 연결되었습니다";
statusElement.className = "connection-status connected";
};
// 메시지 수신 이벤트 핸들러
eventSource.onmessage = (event) => {
console.log("메시지 수신:", event.data);
// JSON 파싱
const notification = JSON.parse(event.data);
// 타임스탬프를 읽기 쉬운 형식으로 변환
const date = new Date(notification.timestamp);
const formattedTime = date.toLocaleTimeString();
// UI에 알림 추가
const listItem = document.createElement("li");
const messageSpan = document.createElement("span");
messageSpan.className = "message";
messageSpan.textContent = notification.message;
const timeSpan = document.createElement("span");
timeSpan.className = "timestamp";
timeSpan.textContent = formattedTime;
listItem.appendChild(messageSpan);
listItem.appendChild(timeSpan);
notificationsElement.appendChild(listItem);
};
// 에러 처리
eventSource.onerror = (err) => {
console.error("SSE 연결 오류:", err);
statusElement.textContent = "서버 연결이 끊어졌습니다. 재연결 중...";
statusElement.className = "connection-status disconnected";
};
</script>
</body>
</html>
이 HTML 파일은 간단한 알림 목록 UI를 제공합니다.
EventSource API를 사용해 서버의 SSE 스트림에 연결하고, 메시지를 수신할 때마다 목록에 추가합니다.
새로운 알림이 들어올 때마다 메시지 내용과 함께 시간을 표시해주어 사용자 경험을 향상시켰습니다.
코드의 핵심은 new EventSource("http://localhost:8080/notifications") 부분입니다.
이는 웹 표준 API로, SSE 연결을 설정합니다.
서버 연결이 끊기면 자동으로 재연결을 시도하는 기능도 내장되어 있어 안정적인 실시간 알림 시스템을 구축할 수 있습니다.
실제 환경에서는 서버와 클라이언트가 다른 도메인이나 포트에서 실행되는 경우가 많습니다.
이 경우 CORS 설정이 필요합니다.
Spring Security를 사용하여 CORS와 보안을 설정해봅시다
package com.study.nofitication
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.web.SecurityFilterChain
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.CorsConfigurationSource
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http
.cors { } // CORS 설정 활성화
.authorizeHttpRequests { auth ->
auth.anyRequest().permitAll() // 모든 요청 허용 (데모용)
}
.csrf { it.disable() } // CSRF 보호 비활성화 (데모용)
return http.build()
}
@Bean
fun corsConfigurationSource(): CorsConfigurationSource {
val source = UrlBasedCorsConfigurationSource()
val config = CorsConfiguration()
// 허용할 오리진 설정 (클라이언트 URL)
config.allowedOrigins = listOf("http://localhost:63342")
// 허용할 HTTP 메서드 설정
config.allowedMethods = listOf("GET", "POST", "PUT", "DELETE")
// 허용할 헤더 설정
config.allowedHeaders = listOf("*")
source.registerCorsConfiguration("/**", config)
return source
}
}
이 설정 클래스는 Spring Security를 사용하여 CORS를 구성합니다.
특정 오리진(여기서는 localhost:63342)에서의 요청을 허용하고, 모든 엔드포인트에
대한 액세스를 허용합니다.
실제 프로덕션 환경에서는 보안 요구사항에 맞게 이 설정을 조정해야 합니다.
마지막으로 Spring Boot 애플리케이션의 설정을 추가합니다.
spring.application.name=Notification
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.check-template-location=true
이 설정 파일은 애플리케이션의 이름을 지정하고, Thymeleaf 템플릿 엔진 설정을 추가합니다.
이제 모든 코드가 준비되었으니 애플리케이션을 실행하고 테스트해 본다면..
알림을 발송하면 브라우저에서 실시간으로 알림이 표시되는 것을 확인할 수 있습니다!
다음과 같은 시나리오로 테스트해 볼 수 있습니다.
지금까지 Spring Boot와 SSE를 활용하여 간단한 실시간 알림 시스템을 구현해 보았습니다.
이 기본 구현을 바탕으로 다음과 같은 기능을 추가해볼 수 있습니다.
실시간 알림 시스템은 현대 웹 애플리케이션에서 사용자 경험을 크게 향상시키는 핵심 기능입니다.
SSE는 단방향 통신이라는 제약이 있지만, 알림과 같이 서버에서 클라이언트로의 데이터 푸시만 필요한 상황에서는 웹소켓보다 간결하고 효율적인 해결책이 될 수 있다 생각이 듭니다.
마치 메시지를 받는 데만 관심이 있는 뉴스레터 구독자처럼, 클라이언트는 편안하게 알림을 받아볼 수 있게 되었습니다.