Serverless에서 SQS 셋팅하고 사용하기

KIM SOO MIN·2023년 12월 11일

bugReport

목록 보기
12/12
📌 AWS 개발자 가이드와 Serverless 가이드를 참고하여 적용한 코드에 대해서 기록합니다.

🗝 SQS를 사용한 이유

  • 매일 오전 10시에 미결제를 재시도하는 스케쥴러가 돌아가고 있었다. 아임포트 결제사를 통해 결제를 시도하는데, 한번에 500개의 결제를 시도했을 때 문제가 없었으나 그 이상의 결제건에 대해서 정상적으로 결제가 진행될지는 검증되어 있지 않았다.
    • 아임포트는 트래픽이 갑자기 증가한다거나의 이유로 종종 결제를 실패하게 되는데, 그 때에 에러처리는 아직 서버에서 해주지 않고 있기 때문에, 어떤 알 수 없는 이유로 결제가 두번 되는 위험이 있는 것이다.
  • 결제 사이에 딜레이를 줄 수 있는 방법이 필요했다.
  • 매일 4천여개의 결제 요청이 정상적으로 들어가고, 따로 변수를 넣어준다거나 이런 신경을 쓰고 싶지 않았다.

💁 FIFO vs Standard

💡 사실 나는 처음에 SQS의 기능 자체를 이해하기 힘들었다. 대기열 메세지 서비스? 메세지 대기열 서비스? 아무튼 SQS는 AWS 개발자 문서에서 한글과 영어로 설명이 되어 있음에도 이해가 어려웠다.

코드 예시를 들어주는데 죄다 'Mark'에게 'Hello!'라는 메세지를 보낸다. 같은 예시 뿐이어서, 정말 문자를 보내는 기능인가..? 이런 착각 속에 빠져 있을 즈음,
메세지 대기열이라는 이름을 보고 예약 문자 정도 구나! 단단히 착각에 빠져버렸다.

뭐, 반은 맞고 반은 틀리다.

👶 FIFO

  • 정확히 1회 처리
  • 선입 선출 배송
  • Standard보다 높은 요금

😎 Standard

  • 순서가 보장되지 않음
  • 어쩌다 한 번 두 개 이상의 메세지 복사본이 전달 될 수 있음

📚 FIFO

결제를 시도하는 메세지이므로 순서는 상관 없으나 정확히 1회 처리를 해야 하기 때문에 FIFO를 채택하여 구현하였다.

naming

  • .fifo로 끝나야 한다.

Delay

  • FIFO는 개별 메세지에 대한 delay를 지원하지 않는다.
  • Delivery delayReceive message wait time은 대기열의 모든 메세지에 대한 것으로, 지정한 시간이 지나면 한번에 메세지를 처리한다.
    • 예를 들어 1시 1초에 5개의 메세지를 delay를 5초로 전송했다면, 1시 6초에 5개의 메세지가 전부 날아간다.
  • Lambda가 메세지를 한번에 하나씩만 처리하게 하고 싶으면, Lambda event에서 Batch size를 1로 조정한다.

결국..

  • 개별 메세지의 대한 delay는 불가능 하나, batch size를 조절하여 한번에 하나씩만 처리하게 하여 delay는 필요 없다고 판단해 FIFO로 구현했다.
💡 개별 메세지에 delay를 줄 수 있는 Standard로 변경 해서 구현을 해보기도 했다.

SendMessage를 할 때 DelaySeconds를 메세지 마다 각각 보내주는 방식으로 개별 delay를 주는 방법이다.

  • 구글링을 해보니 많은 사람들의 예시에서 sendMessage를 하기 전에 대기열에 있는 메세지에 대한 정보를 가져오고, 그 결과값을 보고 delay를 주는 방법을 사용하고 있었다.
  • 해당 방법을 사용하여 테스트를 해보았을 때, 나는 Promise.all을 사용하여 구현해서 그런가 대기열에 있는 메세지를 검색 → 메세지 보내기 하는 과정에서 대기열에 있는 메세지의 양을 정확히 알 수 없었고, 메세지의 숫자는 0으로 나왔다.
  • 그래서 대기열의 메세지 양과 상관 없이 루프를 하나씩 돌 때마다 index를 증가시켜 10개당 1초의 delay를 줄 수 있게 구현하였다.
  • 또한 중복 방지를 위하여 DB에 lock을 걸어 DB가 중복으로 변경되는 것을 막았다.

👀 YAML File 설정

SQS 생성

resources:
  Resources:
    QueueQueueQueue:
      Type: 'AWS::SQS::Queue'
      Properties:
        ContentBasedDeduplication: true
        DelaySeconds: 0
        FifoQueue: true
        QueueName: "queueName.fifo"
  • ContentBasedDeduplication : 대기열에 있는 메세지의 내용이 동일하면 해당 메세지는 보내지지 않는다.
  • DelaySeconds : 대기열에 delay를 준다. 개별 메세지에 대한 delay는 아니다.
  • FifoQueue : FIFO 형식을 사용할건지
  • QueueName : Queue이름. FIFO는 뒤에 .fifo를 꼭 붙여야 한다.

Iam Role 설정

iamRoleStatements:
	- Effect: Allow
      Action:
        - sqs:SendMessage 
        - sqs:ReceiveMessage 
        - sqs:DeleteMessage 
        - sqs:GetQueueAttributes 
      Resource:
        Fn::GetAtt: [ QueueQueueQueue, Arn ]
  • Fn::GetAtt: : arn을 직접 쓰지 않고 arn을 가져 올 수 있다.

SQS Lambda Trigger 설정

functions:
  queue:
    handler: queue.queue
    name: Backend-Fn-${self:provider.stage}-queue
    events:
      - sqs:
         arn:
            Fn::GetAtt:
              - QueueQueueQueue
              - Arn
         batchSize: 1
         maximumBatchingWindow: 0

가장 많이 헤맸던 부분이다. 😡

  • trigger를 설정할 때 아무리 문서 대로 해도 생기지 않았다. deploy를 해보아도 에러 메세지조차 뜨지 않고, 아무런 변화가 없었다.
  • 구글링을 해본 결과 나와 같은 상태인 사람들이 많았고, 문제는 띄어쓰기 때문이었다.
  • 보통 yaml 파일을 작성할 때, tab을 사용하여 띄어쓰기를 했는데, serverless 개발자 가이드의 예제 코드를 아주 유심히 보면 trigger를 설정 할 때 space 3개 씩 띄어져 있는 것을 볼 수 있다. 이걸 어떻게 알아!
  • batchSize : 한번에 메세지를 몇개씩 처리할 지에 대한 설정이다.
  • maximumBatchingWindow : 메세지 수집 시간이다.

👌 Send & Receive & Delete

sendMessage

let sendParams = {
    QueueUrl: url,
    MessageGroupId: groupName,
    MessageBody: JSON.stringify(body)
}
  • QueueUrl : queue의 url이다. .bash_profile에 저장해놓고 사용하였다.
  • MessageGroupId : FIFO를 사용하기 위해 필수로 필요하다. 대기열 그룹의 아이디이다.
  • MessageBody : 보낼 메세지이다. String 형식만 가능하며, 다른 형식은 message attributes에 넣어 보내주어야 한다.

receive

{
    "Records": [
        {
            "messageId": "c451c9e2-00000",
            "receiptHandle": "AQEBs1ugEzqRC1yWjeMAvq***",
            "body": "{\"key\":2}",
            "attributes": {
                "ApproximateReceiveCount": "1",
                "SentTimestamp": "162704**",
                "SequenceNumber": "18863***",
                "MessageGroupId": "retry_unpaid",
                "SenderId": "AROA5KBD****",
                "MessageDeduplicationId": "56a778e****",
                "ApproximateFirstReceiveTimestamp": "1627***"
            },
            "messageAttributes": {},
            "md5OfBody": "3b9f0c439***",
            "eventSource": "aws:sqs",
            "eventSourceARN": "arn:aws:sqs:a****.fifo",
            "awsRegion": "ap-northeast-2"
        }
    ]
}
  • 메세지를 받으면 위의 형식으로 받아오게 된다.
  • 이전의 event와는 다른 형식이기 때문에, 기존의 handler는 사용할 수 없어서 새로운 handler를 만들어서 사용하였다.
  • 내가 필요한 데이터는 receiptHandle, MessageGroupId, body였다.

deleteMessage

let deleteParams = {
    ReceiptHandle: params.receiptHandle,
    QueueUrl: params.url
}
  • 메세지를 받아도 queue는 자동으로 메세지를 삭제하지 않고 다시 대기열로 돌아가게 된다.
  • 결제 실패가 된 건에 대하여 매일 다시 결제를 시도해야 하기 때문에, 다음날 동일한 내용의 메세지를 보내야 했다.
  • 받아온 메세지에서 receiptHandle를 사용하여 해당 메세지를 지워준다.
profile
3년차 풀스택 엔지니어입니다.

0개의 댓글