WebSocket과 Kafka를 이용해 실시간 알림 서비스를 만들어보자

코틀린스프링
By Jeongmin Seo9월 12일, 2023년

목차

    서비스에서 발생하는 이벤트를 사용자에게 실시간으로 전달하기 위해,
    Kotlin, Spring Boot, Kafka, WebSocket을 기반으로 알림 전송 서비스를 구축한 경험을 공유해보자.

    1. 왜 만들었나?

    항공 서비스 백오피스를 개발하면서 항공편 상태 변경, 신규 예약, 내부 공지 등 다양한 이벤트를 특정 사용자 그룹에게 실시간으로 전달해야 하는 요구사항이 있었다.

    해당 프로젝트 내에서 WebSocket 연결을 관리하고 알림 로직을 작성하는 건 부적절하다 생각해 알림만을 위한 별도 서비스를 만들었다.

    당시에는 MSA 환경이 아니었고 대용량 트래픽도 없었다.
    다만 Kafka는 서버 장애 시에도 메시지 유실 없이 안정적이며 대용량 알림 전달에도 용이하다.
    또한 혹여나 미래에 MSA 전환 시 별도 작업 없이 시스템을 그대로 활용할 수 있기에 Kafka를 써보았다.

    2. 프로젝트 목표

    • 디커플링(Decoupling): 알림을 생성하는 서비스와 알림을 전송하는 시스템을 완전히 분리한다. 서비스는 Kafka에 메시지를 보내기만 하면 된다.
    • 실시간성: Kafka를 이용해 실시간으로 이벤트를 수신하고, WebSocket을 사용해 클라이언트로 즉시 발송한다.
    • 확장성 및 안정성: Kafka 메시지 큐를 통해 알림 트래픽을 안정적으로 처리하고, 시스템 장애가 발생해도 데이터 유실을 최소화한다.

    3. 프로젝트 구조

    표준 Gradle/Kotlin 서비스 구조를 따르며, 주요 기능별로 패키지를 분리했다.

    └── src └── main └── kotlin └── {comapny_domain}/notificationtransferlibrary/ ├── global/ # JWT 등 글로벌 설정 │ ├── provider/ │ └── security/ └── notification/ ├── adapter/ │ └── inbound/ │ └── NotificationListener.kt # Kafka 메시지 수신 ├── application/ │ ├── dto/ │ └── usecase/ │ └── NotificationUseCase.kt # 비즈니스 로직 처리 ├── domain/ │ ├── aggregate/ │ └── enums/ └── infrastructure/ ├── config/ # Kafka, WebSocket 설정 ├── handler/ │ └── NotificationWebSocketHandler.kt # WebSocket 연결 및 메시지 전송 └── mapper/

    주요 파일 설명

    • NotificationListener.kt: 지정된 Kafka 토픽을 구독(Subscribe)하고, 새로운 메시지가 들어오면 이를 받아 NotificationUseCase에 전달한다.
    • NotificationUseCase.kt: 수신된 메시지를 파싱하고, 어떤 사용자에게 알림을 보낼지 결정하는 등 핵심 비즈니스 로직을 처리한 뒤 NotificationWebSocketHandler를 호출한다.
    • NotificationWebSocketHandler.kt: WebSocket 연결 수립, 세션 관리, 실제 클라이언트로 메시지를 푸시하는 역할을 담당한다.
    • WebSocketConfig.kt / KafkaConsumerConfig.kt: 각각 WebSocket과 Kafka Consumer에 대한 Spring 설정 클래스다.

    4. 의존성 설정: build.gradle.kts

    서비스의 핵심 기능인 WebSocket과 Kafka를 위해 spring-boot-starter-websocketspring-kafka 의존성을 추가한다.

    // build.gradle.kts dependencies { // Spring Boot WebSocket for real-time communication implementation("org.springframework.boot:spring-boot-starter-websocket") // Spring Kafka for message queue integration implementation("org.springframework.kafka:spring-kafka") // Other necessary libraries like Jackson for JSON processing implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") }

    5. 핵심 로직 구현

    아래 코드들은 이해를 돕기 위해 단순화된 예시 코드.

    5.1. Kafka 메시지 수신 (NotificationListener)

    @KafkaListener 어노테이션을 사용해 notification 토픽을 리스닝한다. 메시지를 받으면 NotificationUseCasenotify 메소드로 전달한다.

    // NotificationListener.kt @Component class NotificationListener( private val notificationService: NotificationUseCase ) { @KafkaListener(topics = ["notification"], groupId = "notification-group") fun receive(@Payload data: String) { notificationService.notify(data) } }

    5.2. WebSocket으로 알림 전송 (NotificationUseCase&NotificationWebSocketHandler)

    NotificationUseCase는 전달받은 데이터를 처리해 NotificationWebSocketHandler를 통해 특정 사용자에게 메시지를 보낸다.
    NotificationWebSocketHandler는 내부적으로 WebSocket 세션과 사용자를 매핑하여 관리하고, 해당하는 세션에 알림을 전송한다.

    // NotificationUseCase.kt @Service class NotificationUseCase( private val socketHandler: NotificationWebSocketHandler, private val objectMapper: ObjectMapper, ) { fun notify(data: String) { // 1. 받은 JSON 데이터를 DTO로 변환 val request = objectMapper.readValue(data, NotificationRequestDto::class.java) // 2. 알림을 받아야 할 수신자별로 반복 request.companyReceivers.forEach { receiver -> // 3. WebSocket 핸들러를 통해 메시지 전송 요청 socketHandler.sendNotifyMessage( NotificationPayloadVo( // "어디로 보낼지"에 대한 정보 (Topic) targetTopic = TopicUtils.createTopicForSend(receiver.workspaceCode, receiver.companyId), // "무엇을 보낼지"에 대한 정보 (Message) message = request.message ) ) } } }

    6. 서비스 사용법

    이 서비스는 별도의 Zookeeper 및 Kafka 서버가 이미 구동 중인 환경을 전제로 한다.
    또한 이 서비스는 독립 실행 가능한 Spring Boot 애플리케이션으로 배포된다.

    1. 설정: application.yaml 파일에 Kafka 서버 주소, JWT 시크릿 키 등 환경에 맞는 설정을 구성한다.
    2. 실행: 빌드된 jar 파일을 서버에서 실행한다.
    3. 연동: 다른 마이크로서비스에서는 알림이 필요할 때, Kafka의 notification 토픽으로 정해진 포맷의 JSON 메시지를 보내기만 하면 된다.

    클라이언트(웹 프론트엔드)는 이 서비스가 제공하는 WebSocket 엔드포인트로 접속하고, JWT 토큰을 통해 자신을 등록(Registry)하여 실시간으로 알림을 수신할 수 있다.

    7. 마무리

    WebSocket과 Kafka를 이용해 중앙화된 알림 서비스를 구축함으로써,
    한 프로젝트 내 알림 관련 코드를 일원화했다.
    다른 개발자들은 알림 전송 로직을 여기저기 작성할 필요 없이 비즈니스 로직 구현에 집중하고, 적절한 시점에 카프카 이벤트 발행만 하면 끝이기에 나쁘지 않아 보였다.

    다만 Kafka를 도입해봤다는 경험은 쌓았지만,
    MSA 환경에서의 통신, 대용량 트래픽을 분산 처리를 위한 설계, 그에 따른 확장성 검증 단계까지는 직접 경험하지 못해 아쉬움이 남는다.