Refactor

[Distance] SQS를 도입해보자.

EJUN 2024. 10. 24. 17:24

 

 

지금 프로젝트를 진행한지 어엿 반년이 지나고 800명의 유저와 3만건에 가까운 채팅을 많은 사용자들이 이용을 주었다..ㅎㅎ

근데 가장 많이 들었던 아쉬운 점 중 하나가 "알림이 고장난거 아니에요?" 라는 피드백이었다.

 

왜 그럴까?

 

원래 초창기에 저희가 사용했던 알림 방식은 FCM을 통해서 채팅이 오면 바로 채팅내용을 카0오톡 처럼 바로 바로 보내주는 형식이었다.

하지만 이렇게 하다보니 FCM자체가 동작을 안할 때가 있었고, 순서도 뒤죽박죽에 이미 갔던 문자알림이 또 가는 버그가 있었고, 무엇보다
이렇게 했을 때 채팅하나 보내는데도 많은 쿼리가 나가는데 이에 FCM까지 전달하려고 하는게 아닌 거 같아 방식을 바꾸게 되었다.

 

그럼 어떻게 보냈냐?!
특정 유저에게 메시지가 오면 FCM Table에 저장을 하고 스케줄러를 통해 30초 마다 한번씩 알림을 전송하고 있었다.

물론 사용자관점에서는 불친절하다는 것을 알고 있었지만 최대한 알림이 정상적으로 가게끔 하고 싶은 마음에 이렇게 로직을 구현했다...

 

FCM 수정 전 알림 전송 방식

이건 개발자인 내가 봐도 정말 불친절한 알림이다. (바로 바로 알림 X, 메시지내용X ㅋㅋㅋ.....)

 

그래서 이젠 다른 방식을 도입하기로 하였다.

바로 AWS에서 제공해주는 SQS(Simple Queue Service)를 활용해 FCM을 전송하는 것이다.

 

흐름은 굉장히 간단하다.

유저가 채팅 전송 -> SQS Queue에 전달 -> Lambda를 통한 FCM 전송

놀랍게도 이게 전부다.

 

 

그 전에 간단하게 SQS에 대해 알아보자.

SQS 동작 방식

구글에 검색하면 AWS에서 제공하는 완전 관리형 메시지 대기열 서비스라고 많이 나온다.

음,,, 좀 더 쉽게 이해를 할 수 있게 자체 번역을 하면 서버끼리 주고 받는 메시지를 잠시동안 보관해주는 우체통 같은 역할을 한다고 생각하면 된다.

즉, A라는 서버가 SQS(우체통)에 메시지를 넣으면 B가 그 메시지를 꺼내서 처리를 해주도록 도와주는 게 바로 SQS다.

그럼 장점이 뭐냐? 라고 물어본다면
전 서버들이 동시에 실행되지 않아도 서로 메시지를 주고 받을 수 있다! 라고 말할 것 같다.

 

정말 간단하지 않나요..ㅎ

 

어 그럼 만약 메시지가 여러개 쌓였는데 이럴 땐 어떻게 처리하나요? 라고 의문을 가질 수 있다.

 

저도 이 궁금증을 갖고 알아봤는데 이때 SQS의 대단함을 다시 한번 느꼈다..

 

SQS는 2가지의 유형을 가지고 있다.

📌 표준 대기열
가장 기본적인 메시지 대기열 유형이라고 부른다.
즉, 빠르게 메시지를 주고 받을 수 있는 보관함이라고 생각하면된다.

📌 FIFO 대기열
이름 그대로 FIFO방식으로 메시지를 전달해주는 보관함이다.

 

그럼 위의 2가지는 대체 어떤 차이를 갖고 있고 Distance 서비스에는 어떤 유형을 선택했고, 그 이유에 대해 말해보겠다.

 

우선 표준 대기열은 빠르게에 집중했기 때문에 순서가 정확히 보장되지 않는다는 특징이 있다.

또한 하나의 메시지가 중복 전달될 수도 있다.(SQS의 표준 대기열은 at least once방식을 사용)

 

반면에 FIFO대기열은 순서가 보장되고, 중복 메시지 제거를 보장해준다.

 

특징 표준 대기열 FIFO 대기열
메시지 순서 보장 순서 보장 X 순서 보장 O
중복 메시지 발생 가능성 중복 메시지 발생 가능성 O  중복 메시지 발생 가능성 X
처리량 무제한 처리 가능 초당 최대 300건(메시지 그룹 사용 시 3,000건) 
메시지 그룹 기능 지원 X 메시지 그룹ID를 사용해 순서 지정 가능
사용 예시 순서가 중요하지 않은 대량 작업 처리 순서가 중요한 작업(금융 거래)

 

 

표준 대기열은 왜? 순서를 보장해주지 않을까?
아까 위에서 말했듯이 표준대기열 방식은 at least once방식이라고 했는데 이를 해석하면 메시지를 최소 한번 이상 전달하는 것을 보장한다라는 의미다. 

즉 SQS는 메시지 손실을 방지하기 위해 중복을 감수하고 무조건 한번은 저장된다 라는것을 의미한다.

 

 

그럼 FIFO는 어떻게 순서보장을 해줄까?

AWS 공식문서를 보면 아주 친절하게 설명을 해주고 있다.

 

출처 : https://aws.amazon.com/ko/blogs/compute/solving-complex-ordering-challenges-with-amazon-sqs-fifo-queues/

여기 사진처럼 3개의 작업을 Queue가 받고 소비자는 들어온 순서대로 받게 된다.

하지만 지금은 소비자가 한명이라 SQS의 특징 상 소비자가 메시지를 삭제할 때 까지 메시지 배치(소비자)를 진행 중 이라고 판단을 한다.

즉, 첫번째 배치가 삭제될 때 까지 다음 배치의 메시지를 release하지 않는다는 것인데 그럼 만약 여러배치가 있을 땐 어떻게 될까?

출처 : https://aws.amazon.com/ko/blogs/compute/solving-complex-ordering-challenges-with-amazon-sqs-fifo-queues/

 

자 이 경우에 메시지들이 11개가 있고, 10개씩 보낸다고 했을 때 2개의 배치가 필요하고

출처 : https://aws.amazon.com/ko/blogs/compute/solving-complex-ordering-challenges-with-amazon-sqs-fifo-queues/

위 처럼 구성이 될 수 있다.

여기서 그럼 두번째 배치는 언제 실행이 될까?
바로 첫번째 비치가 삭제된 후에 두번째 배치를 실행할 수 있다.

이것이 SQS에서 제공하는 순서 보장이다.

 

근데 여기서 만약 병렬로 작업을 해야할 땐 어떻게 해야할까? 즉, 여러 명의 소비자가 있는 경우이다.

출처 : https://aws.amazon.com/ko/blogs/compute/solving-complex-ordering-challenges-with-amazon-sqs-fifo-queues/

위의 사진을 보면 Message Group이라는 걸 볼 수 있다.

소비자는 Message Group라는 것을 통해 그룹 별로 정렬된 메시지를 받고 B그룹메시지를 먼저 처리하고 A그룹 메시지를 처리할 수 있다.

 

이렇게 메시지그룹을 설정하면 여러명의 소비자들도 이용할 수 있게 된다.

출처 : https://aws.amazon.com/ko/blogs/compute/solving-complex-ordering-challenges-with-amazon-sqs-fifo-queues/

이게 바로 여러 명의 소비자가 있는 경우이다.

일단 SQS는 동일한 그룹은 병렬처리가 되지 않는다는 특징이 있다.

 

그래서 위처럼 10개씩 작업을 한다고 했을 때 A그룹에서는 10개가 작업하고 마지막으로 A11이 Queue에 남게 된다.

 

그럼 여기서 질문?!

두번째 배치에는 왜 B11,A11이 있을까..?

그것은 바로 SQS 메시지 그룹 간 병렬처리 방식에 대한 것이다.

 

잘보면 첫번째 배치에서 병렬로 동시에 실행이 되고 각 그룹에 A11, B11이 남게 된다.

이때 SQS는 굳이 2명의 소비자가 개입하지 않고 한명의 소비자가 남은 2개의 메시지를 갖고가서 한번에 처리하는 방식인 것이다...(👍)

 

그럼 또 질문,,!

어 그럼 순서가 보장 안 될수도 있는 거 아닌가..?

정의를 다시 살펴보면 "그룹 내 순서 보장" 이라는 특징이 있다.

즉, A11, B11은 다른 그룹이기 때문에 서로의 순서는 중요하지 않기 때문이다.

출처 : https://aws.amazon.com/ko/blogs/compute/solving-complex-ordering-challenges-with-amazon-sqs-fifo-queues/

위 방식은 남은 메시지를 각각의 소비자에게 분배해서 메시지를 처리하는 작업이다.

 

2가지의 장 단점이 있는데

첫번쨰 방식은 메시지 처리의 단순성과 최소한의 네트워크 요청을 사용하는 장점이 있지만, 

단점으로는 B소비자가 여전히 대기중일 때 A소비자가 모두 처리하면서 B소비자의 활용도가 떨어진다는 단점이 있다.

 

두번 째 방식은 리소르 활용도와 병렬 처리 성능을 선호한다는 장점이 있지만,

단점으로는 두 메시지를 따로 처리하므로 네트워크 요청 횟수가 더 많다는 단점이 있다.

 

자 그럼 이제 Distacne에서 어떻게 구현했는지 보여주겠다.

 

기존에는 

fcmService.createFcm(opponent, SET_SENDER_NAME, ADD_WAITING_ROOM_MESSAGE,WAITING);

이렇게 바로 FCM서버에 요청을 보냈다면

sqsService.sendMessage(opponent.getClientToken(),SET_SENDER_NAME,ADD_WAITING_ROOM_MESSAGE);

지금은 이렇게 sqs서버에 전송을 한다.

 

일단 SQS에 보냄으로써 더 이상 FCM에 대한 관리를 코드상에서 구현 할 필요가 사라진다는 장점이 있었다.

 

그럼 SQS에 FCM메시지를 전송했으니 Lambda가 SQS를 Trigger하게 하여서 Lambda가 FCM을 처리하게 하였다.

 

 

그럼 distance에는 어떤 유형을 선택하고 왜 선택했나요?

우선 FIFO방식을 선택했는데
그 이유는 저는 최대한 사용자에게 알림을 순서대로 보내주고 싶다는 생각이 있었고 FIFO방식을 택하였다.

 

물론 너무 과하지 않냐라고 생각할 수도 있지만 그동안 피드백이 "알림이 이상해요", "이미 왔던 알림이 또 와요" 등등 이었기에 최대한 그런 의견들을 반영하기 위해 선택을 하였다..ㅎㅎ 

최종 알림 형식

 

설정 구성

저는 위와 같이 SQS설정을 구성했는데 각각의 구성이유에 대해 설명하겠다.

표시 제한 시간
표시 제한 시간은 메시지를 소비자가 수신한 다음, 다른 소비자가 해당 메시지를 다시 가져가지 못하도록 설정하는 시간을 의미
예를 들어 SQS기본 설정인 30초로 기준으로, 메시지를 수신한 소비자는 30초 이내에 메시지를 처리해야 다른 소비자에게 전달될 수 있다는 걸 의미한다.
-> 저는 단순 FCM을 처리하는 용도니까 짧으면 짧을수록 FCM전송에 실패했을 때 다른 소비자가 메시지를 가져가 다시 처리를 할 수 있기에 최대한 짧게 잡을려고 하였습니다. ( 너무 짧게 잡으면 중복처리가 발생할 거 같아 10초 잡음)

 

전송 지연
전송 지연은 메시지가 대기열에 추가된 후 소비자에게 전달되기까지의 시간을 의미
-> 제가 사용하는 용도의 FCM은 즉각적으로 사용자에게 보여주어야하기 때문에 전송지연 시간을 0초로 잡아 바로 전달되게끔 하였습니다.

 

메시지 수신 대기 시간
메시지 수신 대기 시간은 대기열이 비어 있을 때, 메시지를 수신하는 요청이 얼마나 오래 대기할지를 의미
Polling을 의미하는 설정이기도 하는데 ShortPolling, Long Polling을 설정할 수도 있다.
예를 들어 10초로 설정을 하면 대기열이 비어있을 때 최대 10초 동안 기다리다가 메시지를 수신 혹은 메시지가 없다면 빈 응답을 반환한다.
-> 이거 또한 FCM은 즉각적으로 이루어져야한다고 생각하기에 시간을 길게 잡으면 전송하는데 시간이 걸리기 때문에 0초로 잡았습니다.

 

메시지 보존 기간
메시지 보존 기간은 대기열에 남아 있을 수 있는 시간을 의미
-> FCM은 빠르게 전달되어야하지만 만약 실패했다고해서 1시간 뒤에 다시 전송된다고 했을 때도 이상하게 보일 수 있을 거 같아 10분으로 잡았습니다.

 

최대 메시지 크기
최대 메시지 크기는 말 그대로 대기열에 추가할 수 있는 메시지의 최대크기를 의미
-> 메시지길이를 길게 보낼 수 있기 때문에 최대한 길게 설정을 해두었습니다.

 

이렇게 FCM 전송 방식의 문제점을 찾아가면서 SQS를 사용하는 방법을 채택했는데 다음 포스팅에서는

SQS와 비슷한 Kafka, RabbitMQ에 대해서 포스팅을 해보겠습니다ㅎㅎㅎ