Springboot

분산 소켓 서버 설명 및 구현(Springboot, SockJS, STOMP, Kafka, Redis, Javascript)

Railly Linker 2025. 3. 28. 21:30

- Socket 을 사용하여 채팅과 같은 서비스를 구현한다면, 통신하는 클라이언트 상호간 직접적으로 데이터를 주고받을 수 있는 WebRCT 와 같은 P2P 기술과는 달리 데이터 전송마다 중간에 서버를 거쳐야만 합니다.

 

이로 인하여 모든 서버-클라이언트 구조와 마찬가지로 연결된 클라이언트 수만큼 서버에 부담이 걸릴 수 있는 구조라는 것으로, 소켓 서버 역시 분산 시켜 이러한 부하를 효율적으로 분산시킬 수 있다면 안정적인 Socket 서버를 구축하고 운영 할 수 있을 것입니다.

 

이번 포스팅에서는 분산 소켓 서버를 구축하는데에 필요한 기본 지식을 간단히 알아보고, Springboot 를 사용하여 효율적인 채팅 서버를 구축하는 방법론을 정리해보겠습니다.

 

(소켓 서버 설명)

소켓 서버 - 클라이언트 구조

 

일반적인 소켓 서버의 구조는 위와 같습니다.

소켓 연결을 관리하는 소켓 서버가 하나 존재하고, 이에 연결되는 여러 클라이언트가 존재합니다.

서버는 연결된 클라이언트의 세션 정보를 내부 메모리에 저장해두고, TCP/IP 프로토콜에 따라서 상호 통신하게 됩니다.

 

모든 클라이언트는 서버와 연결된 상태로,

하나의 클라이언트가 메시지를 전달하고, 이를 다른 모든 클라이언트에 전달한다고 가정하면,

 

1. 메시지를 서버로 전달

2. 서버에서 메시지를 수신

3. 서버에서 메시지를 현재 연결된 모든 클라이언트들에게 전송

4. 클라이언트들은 서버에서 보내준 메시지를 수신

 

위와 같은 간단한 방식으로 처리가 될 것입니다.

소켓이라는 기술을 사용하여 위와 같이 처리하는 방식에 대한 설명글은 이전에 작성한 Socket 설명글을 참고하세요.

 

이제, 여기서 발전시켜봅시다.

위와 같은 방식의 소켓 기술의 응용은 매우 단순하고, 단순한만큼 높은 성능을 보장할 수 있습니다.

다만, 채팅 서버를 구현한다고 가정했을 때, 많은 요소가 부족하다는 것을 알 수 있습니다.

 

예를들어 위에서 설명한 메시지 전체 전송 외에도 특정 유저에 대해서만 1대1로 통신하고 싶을 수도 있습니다.

클라이언트 발행 메시지를 특정 위치로 전송

이러한 기능을 구현한다고 하면,

메시지를 전달하는 클라이언트 측에서 메시지를 받는 측의 식별 가능한 정보를 같이 보내줘야 하고,

서버측에서는 이를 해석하여 올바른 위치로 메시지를 보내줘야만 합니다.

 

이러한 기능을 구현한다고 하면 쉽게 할 수도 있을 것입니다.

Json 형식으로 입력 데이터를 구조화한 후에 상호 약속하에 데이터 입출력을 맞추어 해석하면 됩니다.

 

다만, 이러한 '약속'의 형식이 매번 달라질 수 있습니다.

이번에는 {"userPath" : String, "message" : String} 이렇게 약속을 했지만, 다음에는 {"user" : String, "msg" : String} 이렇게 약속을 할 수도 있기에, 동일한 기능을 구현함에 있어서도 매번 변경사항을 엄밀히 확인해야만 하는 것입니다.

 

이외에도 채팅방 입장/퇴장, 채팅방별 메시지 전송 등을 고려하자면 클라이언트 서버간 약속은 더더욱 많아질 것이고, 이에 따른 예외 처리, 처리 방식 또한 다양해질 것입니다.

 

개발의 자유도를 낮추고, 성능을 약간 희생하여 규칙과 패턴을 정립하고 이를 적용한다면 위와 같은 문제는 사라질 것입니다.

 

이러한 통신 방식의 규칙을 정한 것을 프로토콜이라고 부르며, 소켓 통신 기술에서 텍스트 기반 메시지 교환 규칙을 정한 프로토콜로는 STOMP 가 있습니다.

 

 

- STOMP(Simple/Stream Text Oriented Message Protocol) 란,

이름 그대로 websocket 위에서 동작하는 텍스트 기반 메세징 프로토콜입니다.

STOMP 의 주요 특징은

1. 메시지를 프레임 단위로 전송하여 HTTP 와 비슷한 구조를 가짐
2. pub/sub 패턴을 가짐

위와 같습니다.

쉽게 설명하기 위하여, websocket 을 그대로 사용할 때와 비교하자면,

연결 - 메시지 전송(반복) - 연결 종료

websocket 은 위와 같은 단순한 프로세스로, 메시지의 종류, 형태, 방식 등을 정하는 것이 자유롭지만,
서비스를 구현하기 위해 동일한 로직이라도 천차만별의 구현 방식이 있을 수 있습니다.

위에서 설명했듯, 이러한 자유로움으로 인하여 동일한 기능을 개발할 때에도 매번 같은 어려움과 같은 혼란을 겪을 수도 있습니다.

STOMP 의 경우는,
클라이언트-서버 간 보내는 메시지의 형태와 종류를 정형화하였습니다.

STOMP 프레임의 구조는,

COMMAND
header1:value1
header2:value2

Body


위와 같으며,

앞서 설명과 같이 HTTP 와 유사한 형태로,
이 중 COMMAND 의 종류를 살펴보자면,

CONNECT : 클라이언트가 서버와 연결
CONNECTED : 서버가 클라이언트 연결 승인
SUBSCRIBE : 클라이언트가 특정 주제 구독
UNSUBSCRIBE : 클라이언트가 특정 주제 구독 취소
SEND : 클라이언트가 메시지 발행
MESSAGE : 서버가 구독한 클라이언트에게 메시지를 발행
DISCONNECT : 클라이언트가 연결 종료


위와 같습니다.

예를 들자면,

SEND
destination:/topic/chat
content-type:text/plain

안녕하세요!


위와 같은 형태로 메시지를 보낸다면 /topic/chat 이라는 위치로 "안녕하세요!" 라는 메시지를 보내는 것으로 해석되는 것입니다.

이처럼 STOMP 는 그 이름답게 간단하게 메시지 기반의 통신을 위해 필요한 기능들을 구현함에 있어서 가장 효율적인 형식을 정해두었으므로,
채팅 서비스와 같은 대표적인 소켓 통신 서비스를 만들 때 유리합니다.

다만, 위와 같이 정해진 규칙을 준수해야하므로 반드시 형식에 맞는 요청을 보내야만 하기에 쓸모없는 오버헤드가 통신 성능에 악영향을 끼칠 수도 있다는 단점이 있습니다.

 

 

- 본 포스팅에서 STOMP 를 선택한 이유

특정 서비스를 구현할 때에는 해당 서비스에 어울리는 기술을 사용하는 것이 좋습니다.

 

본 포스팅에서는 채팅 서비스를 만드는 것을 기준으로 하여 소켓 서버 구축을 설명하려는 것으로,

채팅 서비스의 특징상 소켓 통신만이 아닌 유저 관리나 이외 다른 HTTP 기반 통신을 사용하는 기능들이 추가될 가능성이 높으므로 이에 대해선 소켓 통신 전문의 서버가 아닌 범용성이 뛰어난 Springboot 프레임워크를 사용하려는 것이며,

 

게임 서버와 같은 실시간성이 최대한 요구되는 서비스가 아닌 다소 응답시간에 있어 관대한 서비스이기 때문에 유지보수 및 확장성에 영향을 끼칠 수 있는 코드 구조의 통일성을 가져다주는 STOMP 를 적용하려는 것입니다.

 

 

- Redis 를 이용한 분산 서버 구축 설명

분산 서버는, 서버의 수평적 확장(Scale Out)을 통해 서비스를 제공하는 하드웨어 자원의 규모를 동적으로 유연하게 제어할 수 있게 해주는 기술입니다.

 

예를들어 게임 서버를 구축할 때, 분산 서버 처리가 되어있지 않다고 하면,

런칭 첫날이나 특정 이벤트 시점에 사용자가 폭증할 경우 유연하게 대처하지 못하고 서비스 장애를 해결하기 위해서는 더 좋은 성능의 서버 하드웨어를 준비해야 하겠지만, 분산 서버 처리를 통하여 수평적 확장이 가능하다면,

통신량이 늘어난 것을 감지하고 런타임에 바로 가용한 서버를 추가시킬 수 있고,

반대로 사용량이 없을 때에는 연결된 서버를 제거하여 운영 비용을 유연하게 조절 가능하게 되는 것입니다.

 

이러한 분산 서버를 만들 때 필수 요소는,

서비스에 영향을 끼칠 수 있는 상태값이 각 서버별 따로 존재해서는 안된다는 것입니다.

 

예를들어 인증/인가의 로그인 세션을 구현할 때, 로그인된 유저 정보가 한 서버에만 존재한다면, 복제된 다른 서버에 api 를 요청할 때, 로그인이 안 되어 있는 상태로 처리가 될 것입니다.

 

이를 해결하려면 상태값을 모든 서버에서 공유해야만 합니다.

데이터베이스를 사용한다면 데이터를 여러 서버에서 공유할 수 있을 것이지만,

빠른 입출력이 필요한 상태 데이터는 메모리상에 올려 사용해야 합니다.

 

이렇게 여러 위치에서 접근 가능한 인메모리 데이터베이스가 바로 Redis 입니다.

Redis 정보 공유 구조

 

이를 이용하여 위에서 예시를 든 로그인 정보 상태 처리를 한다면, 클라이언트의 로그인 세션 정보를 서버 메모리에 저장하여 사용하는 것이 아닌, Redis 안에 저장해서 사용하면 상태 데이터가 모든 서버에 공유되어 분산 서버가 성립하며, 서버를 그대로 복제하더라도 바로 사용이 가능해지는 것입니다.

 

Redis 에 대한 설명글은 본 블로그 내의 관련된 포스팅을 몇가지 확인하여 참고하실 수 있습니다.

 

 

- Kafka 를 이용한 MSA 간략 설명

Redis 를 사용하면 위와 같이 서버 복제가 가능해집니다.

동일한 서비스를 제공하는 서버를 그대로 복제하여 활용할 수 있는 것입니다.

 

그런데 서버 복제가 아닌 전혀 다른 서비스를 연동할 때에는 어떻게 할까요?

본 포스팅 주제와는 조금 떨어져있지만, Kafka 라는 미들웨어의 주요 사용처를 알아보고 Kafka 라는 것에 대해 이해해봅시다.

 

Kafka 는 MSA 를 구축할 때에 주로 사용됩니다.

서로 다른 서비스이지만 상호간 하나의 서비스와 같이 매끈하게 연동되어야 하는 서비스가 있을 수 있습니다.

예를들어 멤버 정보를 관리하는 모듈이 있고, 그러한 멤버 정보를 이용하여 물품 판매 정보를 관리하는 모듈이 있다고 합시다.

모듈간 DB 엔티티 사용

 

두 모듈은 각자 다른 역할을 수행하지만, 물품 판매 정보 관리 모듈이 멤버 정보 관리 모듈에 종속되어 있다는 것을 볼 수 있습니다.

 

회원 정보 관리 모듈은 회원 정보 입력/수정/출력/삭제를 담당하고,

물품 판매 모듈은 어떤 회원이 어떤 물품을 샀는지에 대해여 처리를 담당하므로,

 

즉, 회원 정보가 변경된다면 물품 정보 역시 수정을 해야만 하는 것입니다.

 

단적인 예로, 회원이 탈퇴한 순간 물품 판매 정보는 삭제 처리가 되고, 삭제 처리가 된다면 특정 상태로 진행중인 거래 정보의 경우 자동 환불 처리가 되어야 한다고 가정합시다.

 

회원 탈퇴 처리를 수행하는 회원 정보 관리 모듈은 본인이 담당한 기능 외에, 회원 정보의 변화가 다른 모듈에 어떤 의미를 지니는지에 대해 알수 없고, 깔끔한 구조를 위해서는 알아서도 안되는 상황입니다.

 

물품 판매 모듈은 회원 정보가 탈퇴된다면 처리해야 하는 프로세스를 알고있지만, 반대로 회원이 탈퇴된 시점에 대해 모르죠.

 

이러한 상황에서 상위 모듈인 회원 관리 모듈이 독립성을 유지한 채 물품 판매 모듈이 회원 탈퇴 시점을 인지하려면,

가장 간단하게는 물품 판매 모듈이 일정 시간마다 데이터베이스를 확인해서 해당 회원이 탈퇴되었는지 아닌지를 확인하는 방법도 있지만, 효율적이라고는 할 수 없을 것입니다.

 

결국 모듈간 통신을 구현하여 회원 탈퇴 시점에 메시지를 서버로 전달하는 방법이 효율적입니다.

 

Kafka 가 그러한 역할을 담당하며,

Pub-Sub 구조로, 메시지 수신측에서 특정 topic 을 구독한 상태에서, 송신측에서 해당 topic 으로 메시지를 발행하면, 수신측이 미리 정해둔 핸들러 콜백이 실행되는 구조입니다.

 

kafka 를 이용한 모듈간 연동

 

위의 예시로 치자면, 물품 판매 모듈이 user-delete 토픽을 구독중인 상태에서, 회원 정보 모듈이 특정 회원이 탈퇴했다는 메시지를 발행하면, 이를 받아들인 모듈이 관련된 로직을 처리하면 되는 것입니다.

 

- Kafka 를 이용한 소켓 분산 서버 구축 설명

위와 같이 kafka 가 pub-sub 구조의 통신 모듈이라는 것을 알 수 있었습니다.

pub-sub 구조로 메시지를 입력 순서대로 발행하므로 이를 메시지큐, 혹은 이벤트큐라고 부릅니다.

 

pub-sub 구조와 원리를 보자면 앞서 설명한 STOMP 와 동일하다고 볼 수도 있습니다.

그런데, 분산 소켓 서버를 구축하는 방법을 설명할 때, 왜 kafka 에 대해서 설명을 했을까요?

 

생각해봅시다.

분산 서버를 구축할 때 필요한 것은 서비스에 영향이 있는 내부 정보를 서버 개별이 아닌 공유해야만 성립된다고 했습니다.

그러면 소켓 서버에서 서비스에 영향을 끼치는 내부 정보가 뭐가 있을까요?

 

바로 연결된 클라이언트의 세션 정보입니다.

TCP/IP 프로토콜로 상호 연결된 소켓 세션 정보는, 아쉽게도 Redis 와 같은 공유 메모리에서 공유할 수가 없습니다.

고로, 서버 복제시 서버별 현재 연결된 유저의 세션이 전부 따로 존재하기 때문에 확장이 불가능한 구조이며, 앞서 설명한 Redis 를 이용한 분산 서버를 구축할 수 없습니다.

 

그럼에도 분산 소켓 서버를 구축하려면 다른 방식을 사용하여야 하는데,

세션 정보를 한곳에 모아서 처리할 수 없기에 각 서버가 이를 처리하도록 처리하면 됩니다.

 

네트워크 기술로 비유하자면, 라우팅으로 데이터를 올바른 위치로 전달할 때, 경로 설정 알고리즘으로 범람(Flooding) 방식을 사용하는 것과 같습니다.

 

한 서버에 소켓 메시지가 전달(메시지 + 경로 정보가 포함되어 있음)되면,

동작중인 모든 소켓 서버로 해당 메시지를 전달하고, 메시지를 전달받은 소켓 서버에서 본인의 세션 정보에 해당 경로 세션이 존재한다면 이에 대해 메시지를 발송하는 것입니다.

kafka 를 사용하지 않은 분산 소켓 서버

 

 

위와 같이 구성하면 분산 소켓 서버를 구현할 수 있습니다.

 

다만, 위와 같은 구성은 확장성이 없습니다.

모든 소켓 서버가, 요청을 전달할 다른 모든 소켓 서버의 위치를 전부 숙지해야 하고, 메시지 전달의 경로도 모든 서버가 상호간 연결되어 있어야 하기 때문입니다.

때문에 위 구조는 결합성이 매우 강하여 유연하지 못한 좋지 않은 구조라고 할 수 있을 것입니다.

 

Kafka 를 사용하면 서버간 메시지 공유에 있어서 중간 역할을 대신 맡아주기에 구조를 개선할 수 있을 것입니다.

kafka 를 사용한 분산 소켓 서버 구조

 

위와 같은 구조로 하여, Kafka 의 Pub-sub 구조로 연결되어, 각 서버의 입장에서는 Kafka 한곳을 바라본 상태로 구독한 토픽과 연결하면 되는 견고한 구조를 만들 수 있습니다.

 

분산 소켓 서버의 구조와 이론에 대한 설명은 이것으로 끝입니다.

특히나 Kafka 와 STOMP 는 동일한 pub-sub 구조이므로 코드를 구현시에 보다 깔끔하게 코딩이 가능한데,

아래로는 Springboot 를 사용하여 이를 실제로 구현하는 방식을 보여드리겠습니다.

 

- Springboot 실습

https://github.com/RaillyLinker/Kotlin_Springboot_PortfolioScalableStompSocket

 

위 Github 레포지토리는 분산 소켓 서버 관련 제가 작성한 샘플 프로젝트입니다.

전체 코드는 위의 프로젝트를 확인하시고, 코드 중 중요한 부분만을 설명하겠습니다.

 

(configurations/WebSocketStompConfig)

WebSocket 의 STOMP 설정 클래스입니다.

registerStompEndpoints 함수에서 STOMP 접속 앤드포인트를 /stomp 로 설정하였고,

withSockJS 로, Socket 을 지원하지 않는 브라우저에서 SockJS 를 사용하여 소켓 통신을 할 때를 대비한 처리를 하였습니다.

 

즉, 클라이언트 입장에서는 http://localhost:8080/stomp 이 주소로 연결 요청을 하면 됩니다.

 

JavaScript 클라이언트 코드를 보자면,

var socket = new SockJS('http://localhost:13001/stomp');

 

이렇게 연결하는 것입니다.

 

아래쪽은 별로 수정할 거리가 없는 설정인데,

먼저 prefix 를 설정하였습니다.

 

아래서 설명할, 클라이언트가 pub 할 때에 사용하는 Stomp Controller 에 요청을 보내기 위해서는 /app 이라는 prefix 를 붙이는 것으로, /app 으로 시작하는 요청은 소켓 서버의 STOMP publish 라는 의미가 됩니다.(클라이언트가 메시지를 보낼 때의 주소 체계는 /app 으로 시작한다는 의미)

 

Springboot STOMP 컨트롤러 코드는,

    // 메세지 함수 호출 경로 (WebSocketStompConfig 의 setApplicationDestinationPrefixes 설정과 합쳐서 호출, ex : /app/topic/test-channel)
    @MessageMapping("/topic/test-channel")
    fun appTopicTestChannel(inputVo: AppTopicTestChannelInputVo) {
        service.appTopicTestChannel(inputVo)
    }

    data class AppTopicTestChannelInputVo(
        @JsonProperty("chat")
        val chat: String
    )

이렇게 코딩했다면,

 

javascript 클라이언트 코드로는,

stompClient.send("/app/topic/test-channel", {Authorization : "Bearer aa", "client-request-code" : "SEND"}, JSON.stringify({ 'chat': $("#message").val() }));

 

이렇게 요청한다는 의미입니다.

 

enableSimpleBroker 의 의미는, 클라이언트가 구독하는 주소의 prefix 인데,

/topic 으로 시작된 채널에 구독한다면 이는 채팅방과 같은 의미입니다.

유저가 /topic/chatroom/1 이라는 위치를 구독하고 이곳에 메시지를 전송했다면, 채팅방 1 을 구독중인 모든 클라이언트들에게 메시지가 전달된다는 것이고,

 

/queue 는 개별 메시지 전달의 의미입니다.

아래의 setUserDestinationPrefix 에서 설정한 /session prefix 와 합쳐서, /session/queue/system-error 이런 식으로 시스템 에러 등에 대한 개별 메시지를 보낸다고 할 때에 사용하는 경로입니다.

 

모든 prefix 는 일반적으로 사용되는 STOMP 주소 설정을 따른 것인데,

한가지 제가 다르게 설정한 것은, /session prefix 로, 원래 기본값은 /user 이지만, 저의 경우는 개별 메시지가 유저단위로 나뉘는 것이 아니라 소켓 세션 단위로 나뉘는 것이라 생각했기에 이렇게 설정한 것입니다.

 

클라이언트의 구독 설정은,

                stompClient.subscribe('/session/queue/request-error', function (greeting) {
                    showMessage(JSON.parse(greeting.body).clientRequestCode);
                    showMessage(JSON.parse(greeting.body).errorCode);
                }, {Authorization : "Bearer aa", "client-request-code" : "SUBSCRIBE"});

                stompClient.subscribe('/topic/test-channel', function (greeting) {
                    showMessage(JSON.parse(greeting.body).content);
                }, {Authorization : "Bearer aa", "client-request-code" : "SUBSCRIBE"});

 

이렇게 하고,

 

서버의 topic 채널에 대한 메시지 전송은,

        kafka1MainProducer.sendMessageToStomp(
            Kafka1MainProducer.SendMessageToStompInputVo(
                null,
                "/topic/test-channel",
                Gson().toJson(StompSubVos.TopicTestChannelVo("from simpMessagingTemplate {inputVo.chat : ${inputVo.chat}}"))
            )
        )

 

queue 채널에 대한 메시지 전송은,

        if (destination == "/app/topic/test-channel") {
            // 개별 에러 메시지 발송
            val stompSessionInfoKey = "${ModuleConst.SERVER_UUID}_${sessionId}"
            val stompSessionInfoValue = stompInterceptorService.sessionInfoMap[stompSessionInfoKey]
            if (stompSessionInfoValue != null) {
                kafka1MainProducer.sendMessageToStomp(
                    Kafka1MainProducer.SendMessageToStompInputVo(
                        stompSessionInfoValue.principalUserName,
                        "/queue/request-error",
                        Gson().toJson(StompSubVos.SessionQueueRequestErrorVo(clientRequestCode, 1, "Need Login"))
                    )
                )
            }

            return null
        }

 

이렇게 됩니다.

 

그외에는 메시지 최대 크기 설정이나 송신 버퍼 크기 설정 등을 설정하면 됩니다.

 

(web_socket_stomp_src/StompInterceptor)

이 클래스는 STOMP 요청에 대한 인터셉터입니다.

인터셉터란, 특정 시점에 대한 콜백을 제공해주는 객체라고 생각하면 됩니다.

 

Springboot 프로젝트에서 STOMP 관련 요청이 왔을 때에 어떤 식으로 처리를 할지에 대해 미리 설정하는 공간이며,

본 프로젝트에서는 preSend 콜백만을 override 했습니다.

 

이 콜백은 STOMP 관련 메시지가 전송되기 전에 실행되는 것으로,

when 문을 사용하여 command 에 따라 다른 처리를 하도록 처리하였습니다.

 

이외에는 postSend, afterSendCompletion 과 같은 콜백이 존재하는데, Interceptor 클래스에서는 이를 실제로 처리하지는 않으며, 처리 코드는 Service 클래스에 위임하도록 하였습니다.

 

(web_socket_stomp_src/StompInterceptorService)

인터셉터에 대한 서비스 클래스입니다.

 

본 프로젝트에서는 소켓 연결/해제 시점에 대한 처리, 특정 채널 구독/취소에 대한 처리, 메시지 전송에 대한 처리를 오버라이딩 하였습니다.

 

먼저 소켓 연결시에는 sessionInfoMap 에 세션 정보 객체를 저장하게 하였습니다.

앞서 설명했듯, 소켓 서버는 내부에 연결된 세션 정보를 기반으로 메시지를 전송하므로, 연결 시점에 이 변수에 데이터를 저장하는 것입니다.

 

저장 정보에는 개별 메시지를 보낼 때 사용할 principal 정보, 모든 서버에 걸쳐서 겹치지 않는 세션 UUID 정보, 접속 시간, 멤버 정보(인증/인가 처리에 따라서 성공하면 not null, 실패하면 null)가 들어갑니다.

 

코드를 살펴보신다면 알 수 있듯이,

인증/인가 처리, 메시지 비속어 필터, 메시지 로깅, 메시지 차단 필터 등의 필터링 처리를 주로 작성하면 됩니다.

 

(서버 고유값 및 세션 접속 정보 처리)

이제부터는 클래스 단위가 아닌 기능 단위로 설명하겠습니다.

분산 서버를 구축한다면, 추후 모니터링 및 코드상 서버 분별 처리 등을 해야 할 수 있습니다.

즉, 현재 실행중인 서버 프로세스를 분별할 수 있는 고유 정보가 필요합니다.

 

이 정보는 const_objects/ModuleConst 클래스 내에 전역변수로 SERVER_UUID 에 마련해두었습니다.

서버를 구분짓는 고유성은, 서버가 구동된 시점의 시간 데이터와, UUID 랜덤생성값을 합쳐서 절대로 중복될 수 없도록 구성하였습니다.

 

또한, 모니터링시에는 서버 정보, 그리고 접속된 세션 정보를 알아야 할 경우가 있습니다.

예를들어 통신 장애 파악 시스템을 만들거나, 현재 접속중 여부를 파악할 때 사용되죠.

이를 구현하기 위해서 Redis 공유 메모리에 현재 생성된 세션 정보를 등록하도록 하였습니다.

 

소켓 세션이 등록되면 Redis 메모리 안에 세션 정보를 입력하도록 하였으며,

세션 해제 여부는, 세션 해제 콜백이 실행된 이후만이 아닌, 서버의 물리적 고장 등으로 해제 콜백이 발생하기도 전에 연결이 끊길 가능성이 있기 때문에, Redis 에 일정 시간의 '데이터 생존시간'을 두고 이 시간이 지나가기 전에 세션 정보를 갱신하지 않으면 연결이 끊긴 것으로 간주하도록 설정하였습니다.

 

그러한 갱신 로직은 sys_components/ApplicationScheduler 클래스 안에 Springboot Scheduler 를 통해 주기적으로 정보가 갱신되도록 처리를 하였습니다.

 

(인증/인가 처리)

Springboot 에서 TCP/IP 통신은 HTTP 에 비해 인증/인가 처리를 하는 것이 까다롭습니다.

그래도 STOMP 프로토콜은 마치 HTTP 와 같이 헤더를 지원하므로, JWT 인증/인가를 적용하는 방식과 동일하게 헤더 내에서 Authorization 의 값을 가져온 후, 이를 파악하여 올바른 인증/인가 여부를 판단하도록 처리하였습니다.

 

이에 대한 예시 코드는 앞서 살펴본 STOMP 인터셉터의 서비스 클래스의 connectFromPreSend 콜백 안에 설정되어 있으며,

이러한 방식으로 소켓 통신의 적합성 여부와 기능에 따른 권한 여부를 판단할 수 있을 것입니다.

 

(Kafka 연동 방식 및 메시지 전송 코드)

앞서 이론 설명부분에서 설명한 Kafka 를 이용한 분산 소켓 서버 구조를 구현하였습니다.

kafka_components 폴더 안의 consumers 폴더에는 다른 서버에서 발행한 kafka 메시지를 받는 코드가 있고, producers 에는 내가 클라이언트로부터 받은 메시지를 다른 서버에 발행하는 코드가 있습니다.

 

producer 부터 살펴보자면,

stomp_send-message 라는 토픽으로 JSON String 데이터를 보내면 끝입니다.

입력값에는, 개별 전송일 경우에 누구에게 보낼 것인지를 설정하는 부분인 principalName 변수가 있고, 어느 토픽으로 메시지를 발행할 것인지에 대한 destination 변수와, 그리고 메시지를 의미하는 messageJsonString 변수가 있습니다.

 

이 중 messageJsonString 변수는 말 그대로 어떠한 형식이든 JsonString 형태만 유지하면 받도록 처리하여 모든 형태의 메시지를 받을 수 있도록 한 것입니다.

 

consumer 의 경우는,

producer 에서 보낸 데이터를 받는 부분으로,

이곳에서 실질적인 Stomp 메시지 전송이 이루어집니다.

 

보시다시피 principalName 이 null 인지 아닌지(null 이라면 전체 전송, 아니면 개별 전송)에 따라 사용하는 함수가 달라질 뿐이지, 매우 단순하게 kafka 로 받은 메시지를 STOMP Socket 으로 전달하는 역할만을 수행합니다.

 

위와 같은 방식을 적용함에 따라서,

코드상으로는 kafka 의 producer 만 실행시킨다면 메시지를 발행시킬 수 있으며, convertAndSend 와 같은 Socket 함수는 절대 사용할 필요가 없어진 것입니다.

 

실제로 kafka 를 통해 연결된 모든 소켓 서버에 메시지를 전송하는 예시 코드를 살펴보자면,

services/WebSocketStompService 클래스를 보시면 됩니다.

 

kafka Producer 객체를 가지고, 앞서 구현한 STOMP 메시지 발행 함수에 데이터를 입력해준다면,

 

kafka producer 발행 -> 해당 토픽을 구독중인 모든 서버가 메시지를 받음 -> 만약 해당 메시지를 받을 조건이 충족된 세션이 존재하는 서버라면 해당 세션에 메시지를 발행

 

위와 같은 프로세스로 메시지가 발행될 것입니다.

 

- 이상입니다.

위에서 설명드린 방식대로 STOMP 서버를 만들 시 손쉽게 분산 소켓 서버를 구축할 수 있을 것입니다.

이를 응용하여 메시지 데이터를 MongoDB 와 같은 입출력 성능 좋은 NoSQL 을 사용하는 등의 보다 복잡한 처리를 추가한다면 어렵지 않게 채팅 등의 소켓 응용 서비스를 구축할 수 있을 것입니다.