iOS

[Swift] STOMP 톺아보기

하노 Hano 2024. 4. 22. 13:59

안녕하세요, 하노입니다 :D

오늘은 Websocket에서 사용할 수 있는 Protocol 중 하나인 STOMP (Simple Text Oriented messaging protocol)

에 대해서 간단하게 알아보려고 합니다!

 

사실 회사에서 STOMP 기술을 채택하면서 저도 처음 알게 된 기술인데요,

문서와 함께 간단하게 톺아보는 시간을 가져보려고 합니다!

(Stomp 문서 링크: https://stomp.github.io)

 

 

그럼 시작하겠습니다!!

 


 

1. STOMP 란?

What is it?

STOMP is the Simple (or Streaming) Text Orientated Messaging Protocol.

 

네. STOMP는 단순한 텍스트 지향 메세징 프로토콜입니다. 참 쉽죠?

실제로 사용하는데 클라이언트 기준에선 정말 쉽게 사용할 수 있는 프로토콜이라고 생각합니다!

 

사실 STOMP의 마지막 Release Version은 1.2로 2012년 경에 업데이트되었는데요,

그래서 이미 10년이 넘게 지났으므로 1.2를 기준으로 톺아보겠습니다!

 

 

2. OverView

STOMP는 HTTP를 모델로 한 Frame 기반 프로토콜입니다.

Frame의 구성은 Command, Header, Body로 구성되어 있습니다.

그래서 Command, Header, Body를 구성하고 Frame으로 묶은 후 Text화 시켜서 송•수신하는 방식입니다!

 

뒤에서도 명령어 별로 알아보겠지만, 간단하게 알아보면

 

위 그림처럼 Frame 내에 명령어 (Command)와 header , Body를 조합합니다.

뒤에서 좀 더 자세하게 알아볼께요!

 

 

3. STOMP Frames

뒤에서 알아본다고 해놓고 바로 다음이네요!
Frame에 대해 알아보겠습니다.

 

 

3.1 Frame

Frame의 그림은 위에서 보았던 것처럼 Command, header, body로 구성되어 있는데요, (3번째 말하는 중..)

근데 이걸 어떻게 구성해서 보내냐?? 하면

제일 처음에 STOMP는 단순한 텍스트 지향 메세징 프로토콜이라고 했죠??

그래서 이걸 Text(String)화 시켜서 보냅니다!

 

"""
COMMAND
header1:value1
header2:value2

Body
"""

 

바로 이런 식으로 말이죠!

 

위에 코드 블럭에서 볼 수 있듯이 Command, header, body를 모두 String 타입으로 변환하여 통신합니다!

그래서 Command, header, body의 구분을 줄 바꿈으로 구분하여 Parsing합니다!

 

 

 

3.2 Body

Body는 실제로 메세지가 전달되는 영역입니다.

(HTTP API를 호출할 때 Response parameter와 비슷하다고 보면 될 것 같습니다.)

 

이때 Body는 SEND, MESSAGE, ERROR Command에서만 Body가 포함되어야만 합니다.

그 외에 Command에서는 Body를 포함시키면 안 됩니다!!

 

3.3 Header

3.3.1  - 1.2 version 변경 사항

Header에서 1.2 version에 새롭게 추가된 사항이 있는데요!

바로 content-lengthcontent-type을 header에 추가시켜 주어야 합니다!

 

만약 추가하지 않으면 에러가 발생합니다.. ㅠㅠ 왜냐면 제가 겪어봤거든요..

꼭 인지하셔서 1.2 version에서는 추가해 주세요!

 

 

3.3.2 Receipt

CONNECT Command를 포함하고 있는 Frame을 제외하면,

모든 Frame에 대하여 클라이언트는 receipt를 임의의 값으로 header에 넣어 사용할 수 있습니다!

 

어떻게 사용하냐구요???

 

COMMAND
header1:value1
header2:value2
receipt:message-12345

Body

 

위 코드 블럭처럼 Header 파트에 receipt를 추가시켜서 넣어주면 됩니다!

 

이때 receipt를 추가해서 넣어줄 때 임의의 값을 넣어서 사용할 수 있는데요.

receipt를 추가해서 넣어줄 경우 서버에서 RECEIPT라는 Command로 Frame을 송신해 줍니다!

그러면 이때 receipt id를 비교해서 해당 frame이 정상적으로 전송이 됐군! 이라고 알 수 있겠죠???

 

이 부분은 뒤에서 좀 더 자세하게 다뤄보겠습니다!

 

 

 

 

4. COMMAND (Client)

사실 STOMP는 Command에 따라서 동작이 확실하게 구분되는 장점이 있습니다!

그래서 오늘은 STOMP에서 가장 자주 쓰이는 Command 일부만 알아보겠습니다.

(더 많은 Command를 알아보고 싶으시다면 문서를 참고해 주세요!)

 

Command를 알아보기 전에,

기본적으로 Command를 사용할 때는 대문자를 기준으로 합니다!

 

그럼 진짜 알아보러 가시죠!!

 

 

4.1 CONNECT

처음 Socket을 열고 서버로부터 연결을 요청할 때 CONNECT Command를 송신하여 연결합니다.

 

// 클라이언트 송신 시
CONNECT
accept-version:1.2

 

 

위 코드 블럭처럼 CONNECT에 header를 추가해서 송신하는데요!

이때 앞서 말씀드린 것처럼 Body는 포함시키지 않습니다!

 

그리고 꼭 추가시켜주어야 하는 header가 있는데요.

두구두구~~~

 

바로 accept-version입니다!

accept-version으로 서버에서 version을 체크하기 때문에 꼭 넣어주여야 합니다!

 

만약 클라이언트에서 모든 버전이 사용 가능하다면,

 

// 클라이언트 송신 시
CONNECT
accept-version:1.2,1.1,1.0

 

위 코드블럭처럼 콤마를 넣어서 서버에 송신하면 됩니다.

 

그럼 서버에서도 분명 대응하고 있는 버전이 있겠죠?

 

그럼 서버에서도 버전을 체크 후 교집합의 가장 최신 버전을 선택해서 아래 코드 블럭처럼 보내줍니다!

 

CONNECTED
version:1.2

 

 

이때, 서버와 클라이언트의 버전의 교집합이 없으면 Error가 발생합니다!

 

그리고 선택 가능한 header에서는 heart-beat 가 있는데요.

heart-beat란 서버와 socket 통신 시 ping-pong 개념이라고 생각하시면 됩니다!

 

하지만 분명 선택 가능한 header라고 말씀드렸죠??

만약 heart-beat를 보내지 않을 때 서버에서 자동으로 default값 (10000, 10000)으로 설정합니다!

 

그래서 특별하게 heart-beat를 맞추지 않는 이상 선택적으로 보낼 수 있습니다.

 

그 외에도 header에서 여러 가지 설정을 할 수 있는데요.

자세한 건 문서를 참고해 주세요!

 

 

 

4.2 SEND

다음은 SEND에 대해 알아보겠습니다.

 

SEND는 실제 대상에게 메세지를 보내는 Command입니다.

 

SEND
destination:/queue/a
content-type:text/plain

hello world

 

이때 header에 destination이라는 것이 있는데요.

socket  연결 후 메세지를 보낼 channel (또는 위치) 라고 생각하시면 편할 것 같습니다!

그리고! 여기서 드디어 Body가 들어갑니다! Body로 실제로 보낼 메세지를 넣어주면 됩니다.

 

 

4.3 SUBSCRIBE

그럼 destination으로 메세지는 보내는데 수신은 어떻게 할거냐구요????

사실 SEND Frame을 보내기 전에 먼저 해야 할 작업이 있습니다.

 

바로 Subscribe죠!!

 

SUBSCRIBE
id:0
destination:/queue/foo
ack:client

 

엇! SEND와 같은 header인 destination이 보이네요???

맞습니다~~~ 이 destination channel(또는 위치)로 subscribe를 하면 메세지를 송신할 수 있습니다!

 

그 외에도 idack라는 header가 있는데요.

id는 하나의 socket에서 여러 개의 channel을 Subscribe할 수 있으므로, 이를 구분하기 위한 구분자입니다!

 

ack는 선택적 header입니다!

따라서 header에 선언하지 않아도 됩니다.

ack 모드는 크게 auto, client, client-individual 총 3가지 모드가 있습니다.

이 중에 default는 auto이므로 선언하지 않을 경우 auto 모드로 설정됩니다!

 

 

4.4 UNSUBSCRIBE

방금 subscirbe에서 id header의 기능은 channel을 구분하기 위한 구분자라고 말씀드렸습니다!

이걸 어디에서 쓰이나면 바로 Unsubscribe할 때 쓰입니다!

 

UNSUBSCRIBE
id:0

 

위 코드 블럭처럼 id에 unsubscribe할 channel의 id를 추가해서 보내면 해당 id의 channel만 Unsubscribe 됩니다!!

 

 

 

 

4.5 DISCONNECT 

Disconnect은 말 그대로 연결을 종료할 때 사용하는 Command입니다.

사실 정상적인 Disconnect을 하기 위해서 STOMP에서 권장하고 있는 Flow가 있습니다.

한번 Flow를 따라가면서 알아보겠습니다!

 

1. DISCONNECT Command Frame을 서버로 송신합니다.

DISCONNECT
receipt:77

 

이때 header에 receipt를 추가하여 송신합니다! 

 

 

2. 서버로부터 DISCONNECT Frame에 대한 RECEIPT Frame을 수신받습니다.

RECEIPT
receipt-id:77

 

3. receipt-id를 확인 후 소켓을 닫습니다.

 

 

위 Flow가 STOMP에서 권장하는 방법인데요,

 

만약 서버와 클라이언트가 위 방법을 사용하지 않고 Disconnect과 동시에 Socket을 닫는다면,

클라이언트가 RECEIPT Frame을 수신하지 못하여 정상적인 종료가 아니라고 판단합니다!

따라서 위 Flow로 Disconnect을 하길 권장하고 있습니다!!

 

 

 

5. Command (Server)

자!! 그러면 우리는 클라이언트 입장이니깐!

서버에서는 어떤 Command가 날라오는지 살펴봅시다!

 

5.1 MESSAGE

Message frame은 클라이언트가 특정 channel을 구독할 경우 메세지를 전달하는데 사용되는 Command입니다.

간단하게 말해서 상대방이 메세지를 보내면 MESSAGE Command로 Frame을 수신할 수 있는 거죠!!

 

MESSAGE
subscription:0
message-id:007
destination:/queue/a
content-type:text/plain

hello queue

 

위 코드 블럭이 서버로부터 수신받는 Frame인데요!

 

위에서 다 설명드렸듯이 특별한 header는 없어 보이는데요!

이 중에서 한 가지만 설명드리자면, message-id는 서버에서 주는 message의 고유한 id값입니다!

 

 

5.2 RECEIPT

드디어 나왔네요....!!  앞에서 그토록 말했던 Receipt가 어디에서 쓰이냐면 바로 여기서 쓰입니다..!!

물론 클라이언트가 보내는 건 아니고 서버로부터 수신받는 Frame입니다!

 

RECEIPT
receipt-id:message-12345

 

기본적으로 위 코드 블럭처럼 Frame이 구성되어 서버가 송신합니다.

 

여기서 receipt-id 어디서 많이 보지 않았었나요????

 

바로바로 Disconnect Flow에서 봤었습니다!

사실 Disconnect Flow 말고도 header에서 receipt로 사용했었는데요!

바로 이 receipt를 header에 추가시켜 줄 경우 서버에서 모든 receipt에 대하여 이 RECEIPT Frame으로 송신해 줍니다!

 

일종의

너의 Frame은 정상적으로 서버에 도착했어!

를 알려주기 위한 답장(직역하면 영수증????)인 거죠!!

 

이제 좀 궁금증이 풀렸네요!!

 

 

 

5.3 ERROR

근데 만약에 서버에서 처리 중 정상적으로 클라이언트에서 보내지 않았다면 어떡할까요???

이외에도 서버 오류로 인한 에러 등을 처리하기 위한 Command가 ERROR입니다!

 

ERROR
receipt-id:message-12345
content-type:text/plain
content-length:170
message:malformed frame received

The message:
-----
MESSAGE
destined:/queue/a
receipt:message-12345

Hello queue a!
-----
Did not contain a destination header, which is REQUIRED
for message propagation.

 

위 코드 블럭과 같이 Frame을 클라이언트가 수신받게 됩니다!

뭔가 많아 보이지만 저희 이제 다 구분할 수 있잖아요~~

 

위에 코드 블럭을 해석해 보자면 클라이언트가 receipt-id:message-12345를  포함하는 Frame을 보냈는데

웬걸? destination header는 필수값인데 보내지 않았어!!

 

라고 해석해 볼 수 있겠네요!!

 

여기서 눈여겨봐야 할 것은 receipt-id로 어떤 Frame에서 Error가 발생했는지 알 수 있다는 겁니다.

 

 

 


 

네, 여기까지 해서 길고 긴 STOMP 톺아보기를 해봤습니다..!!

 

사실 내용이 너무 많아서

중간에 클라이언트 입장에서 혹은 iOS 입장에서 사용하지 않거나 크게 중요하지 않은 부분은 과감히 뺐습니다...

안 그러면 톺아보는 것이 아니라 자세하게 주도면밀하게 살펴보기가 될 것 같았거든요...

 

더 궁금한 부분은 중간에 말씀드린 것처럼 문서를 보시는 것을 권장드립니다!

 

 

STOMP는 사실 한국어로 된 레퍼런스를 찾기 힘들어서 직접 블로그로 정리해 봤는데요,

저와 같은 미래에 STOMP 사용자에게 조금이나마 도움이 되길 바랍니다...!!

 

그럼!!