retry recover 적용과정

깡통·2024년 2월 21일
0

1. 요구사항

  • 사용자에게 회원가입 재요청하는일이 최대한 없도록 한다.

당시 상황 - GMail에 지연이나 장애가 생기는경우 고려

메일 전송은 SMTP 프로토콜로 보내고있었습니다.

메일전송 전용서비스(SendGrid, Mailgun), SMTP 프로토콜, AWS SES 등의 방법중 SMTP 프로토콜 방식을 선택한 이유는 아래와 같습니다.

  • 비용이 무료이며 가장 익숙하고 간단한방식 입니다.
  • 기업용이 아닌 개인 메일로 발신할 수 있는 개수가 일일 500개라는 제한(기업용 이메일의 경우 2000개)이 있지만, 실제 사용자를 받으려는 프로젝트가 아니었기에, 토이프로젝트임을 가정하면 충분하다고 판단하였습니다.
    • 혹여나, 예정과 다르게 실 서비스로 운영되어 더 많은 메일을 보내야 한다면 여러 gmail 계정을 만들어 메일 한도초과 예외를 try-catch로 잡아 세컨계정으로 메일전송 해주는 방법이있습니다. 하지만 이 방법은 코드의 복잡도를 고려했을때 일일 1000개정도의 메일전송까지만 커버하는것이 좋아보입니다.
    • 일정 비용을 지급하여 GMass을 이용해 일일 최대 10,000명의 수신자로 늘릴 수 있습니다.

2. 의사결정 순간

  • 사용자에게 재요청 해달라는 방법은 제외했기 때문에, 실패한 메일을 재전송 해주거나 추가 보조메일 서버를 두어 실패 메일을 보내는 방법을 고려했습니다.

  • 메일서버의 일시적 지연과 큰 장애 두가지 경우를 나눠서 생각했습니다.

일일 메일전송한도가 500개라는점 + 일시적인 지연이 발생하더라도 바로 보조메일 서버로 보내진다는 단점을 고려했을 때, 첫번째 메일서버에서 최대한 재시도 해주는 방법 선택을 선택했습니다.제가 선택한 방법도 물론 단점도 존재합니다. 이는 하단에 언급하겠습니다.

그렇다면, 전송 실패한 메일을 재시도 하기로 했으니 재시도 방법들을 고려해야 했습니다.

1. try-catch로 메일 전송 예외 잡아서 직접 재전송하기

  • 가장 간단하지만 재전송 횟수와 텀을 지정할수 없으며 트래픽이 조금만 몰리더라도 지연/장애 상황에 네트워크 트래픽에 부담을 줄 수 있는 단점이 있습니다.
    • 예를들면 메일서버는 10초간의 지연이 발생하는 상황인데 try-catch로 바로잡아서 재전송 하더라도 10초안에 재전송 포함 2번이 전송되면 메일 전송은 결국 실패합니다.

2. 전송 실패한 메일들을 DB나 별도의 큐에 보관하여 스케줄링을 돌며 전송해주기
회원은 요청후 가입메일을 인증을 해야 서비스를 이용할 수 있기때문에 최대한 빠르게 메일을 받아야하는 상황이었습니다.

  • 스케줄링서비스 + 메일을 저장하고 재전송해야할 인프라가 추가됩니다.
  • 메일전송 실패가 발생할 빈도에 비해 해당 관리비용을 추가할 가치가 있을까?
  • 메일전송을 바로 받을 수 있을것인가?
    • 스케줄링 주기를 짧게하면 최대한 가능하겠지만 발생 가능성이 적은 일에 비해 낭비처럼 느껴짐

위 내용을 고려했을땐 SpringBoot Retry에 비해 메리트를 느끼지 못했습니다. 조금더 관리비용이 적은 SpringBoot Retry를 선택하게 됩니다.

3. SpringBoot Retry(결정)
2번 방법에 비해 훨씬 간단하고 비용이 적다고 판단했습니다. 재전송 횟수와 텀을 설정할 수있으며, retry 횟수를 소진하고 나면 후순위 작업으로 recover 로직도 작성할 수 있습니다.

retry의 간격을 어떻게 둘것인가?

  • 재시도 자체가 네트워크에 부담을 가중할수있습니다. 만약 정말로 이메일서버에 문제가 생겨 요청자체가 지연되고있는데 모든 클라이언트에서 retry를 동일하게 연속시도한다면 네트워크 트래픽이 훨씬 증가할수있습니다.
  • 이를 방지하기위해 일반적으로 Exponential Backoff 전략을 사용합니다. 말 그대로 지수에 비례해서 retry의 대기시간을 늘리는방법입니다. (100ms, 200ms, 400ms ...)

https://jungseob86.tistory.com/12
https://aws.amazon.com/ko/blogs/architecture/exponential-backoff-and-jitter/

Exponential Backoff만으로 동시요청이 몰렸을 때 전부 커버할수 있을까?

  • 만약 동시에 요청이 몰린다면 똑같은 지수에 비례한 시간 간격으로 retry가 몰리게되어 문제가 생길수있습니다.

  • 이 때문에 Jitter(지연변이)라는 개념이 등장하는데 간단히 말하면 Exponential Backoff로 인한 대기시간 + 랜덤 대기시간을 추가적으로 더해 동시에 retry가 몰리는것을 방지하고 분산시킬수있습니다.

스프링에선 retry를 편하게 사용할수있게 지원해줍니다.

@Retryable의 value들을 설정하여 Exponential Backoff+ Jitter의 개념을 흉내낼수있게 되었습니다
해당 이벤트에서 메일전송중 CustomMessagingException이 발생한다면 retry가 5번동작할것입니다.

    @DisplayName("메일서버에서 예외발생시 retry가 최대 5회 동작한다")
    @Test
    void retry() {
        // given
        doThrow(CustomMessagingException.class)
                .when(mailService)
                .send(any());

        // when
        mailSentEventHandler.handle(event);

        // then
        // 조건이 true가 되거나 시간초과에 도달할때까지 기다리며 테스트에서 비동기작업을 처리/확인방법을 제공
        await().timeout(TIMEOUT_FOR_TEST, TimeUnit.SECONDS).untilAsserted(() -> verify(mailService, times(5)).send(any()));
    }

우리가 테스트해봐야할점은 정말 5번의 retry가 발생하는지, 그리고 retry간의 대기시간이 고정적이고 규칙적인지 확인해야합니다.

결과

Before : 재전송을 위한 간격(s) 1-1-1-1
After : 재전송을 위한 간격(s) 1-3-5-14


Before : 재전송을 위한 간격(s) 1-1-1-1
After : 재전송을 위한 간격(s) 2-2-8-14

테스트는 성공적입니다. retry의 대기시간이 랜덤이고 고정적이지 않습니다. 이로인해 특정시간에 동시에 요청이 몰리더라도 적용하지않았을때보다 비교적 네트워크 트래픽을 줄일수 있게 되었습니다.

retry의 최대 시도횟수를 초과한다면?

현재 retry의 최대시도횟수는 5회로 설정했습니다.만약 5회의 재시도를 시도했음에도 불구하고 메일전송에 실패한다면, 그때는 정말 사용자에게 가입 재요청을 요구해야할까요?

recover

spring에선 이경우 복구로직을 작성할수있는 기능을 제공합니다.

retry가 끝났음에도 CustomMessagingException이 발생한다면 @Recover가 붙은 해당 메서드가 실행됩니다.

3. 잠재적 문제 & 한계

  • 큰 장애가 나서 당분간 복구가 안되는 상황이라면, retry자체가 낭비가 될수있으며 쓸데없는 응답시간이 길어지는 상황이 발생할수있습니다. 이 경우를 고려해야한다면 장애 상황을 감지하고 retry를 끊어낼수 있는 방법에대해 학습할 수 있습니다.

  • Jitter라는 retry의 랜덤 재전송값을 부여하여 순서가 보장되질 않을 수 있습니다. 예를들어 요청1,2가 각각 4시 30분 20, 25초에 메일전송 요청이 들어오고, 요청에 실패해 4시 30분 40초, 4시 30분 37초에 retry될수 있습니다

0개의 댓글

관련 채용 정보