메시지 발송 이중화 여정기

Mar.16.2022 이재훈

Backend

안녕하세요! 우아한형제들 공통시스템개발팀에서 메시지플랫폼을 개발하고 있는 이재훈입니다.
이 글에서는 작년에 진행한 SMS 발송 외부 시스템 이중화 프로젝트 이야기를 담아보았습니다.
어떤 이유로 이중화를 하게 되었고, 어떻게 이 과제를 해결할 수 있었는지에 대한 여정기를 담은 글입니다.

무슨 내용인가요?

  • 작년에 진행한 SMS 발송 이중화 프로젝트의 여정을 담은 이야기입니다.
  • SMS 발송 이중화를 하기 위해 어떤 난관이 있었고 이를 풀어가는 과정을 담았습니다
  • Spring Cloud Config를 이용하여 배포 없이 트래픽을 전환할 수 있었던 이야기를 담았습니다.

2021.06.18 SMS 발송 외부 시스템 장애 발생

2021.06.18 오후 5시.
장애는 언제나처럼 소리 소문 없이 우리에게 찾아왔습니다. SMS를 발송하는 외부 시스템의 장애가 발생하였고, 이로 인해 우아한형제들의 서비스 전역에서 SMS를 발송하지 못했습니다.
대표적인 영향으로 인증 문자 메시지를 발송할 수 없어 운행을 시작하고자 하는 라이더 분들이 앱에 로그인을 하지 못하는 상황으로 연결되었습니다.
이처럼 장애에 대한 대비가 전혀 안된 상황에서는 저희가 할 수 있는 일이 많이 없었습니다.

팀 안에서는 이런 대화를 주고받으며 외부 시스템이 빠르게 복구되기만을 기다릴 수밖에 없었습니다.

“아 이중화만 되어있었더라면…..”

“우리 시스템이 제 역할을 하지 못하는데 제가 할 수 있는 게 없네요.”

“아쉬움은 항상 장애 상황에 크게 느껴지네요”

저희 팀에서는 장애가 해소되는 시간 동안 많은 무력함을 느꼈습니다.
장애로 인해 저희는 큰 동기부여를 받게 되었고, 다른 어떤 업무보다 이중화 작업을 최우선으로 하여 프로젝트를 시작하게 되었습니다.

이중화를 가로막는 요인

이중화라는 목표를 달성하기 위해서는 어떤 기능이 개발돼야 달성할 수 있을지 고민해 보았습니다.
완벽한 이중화 구성을 위해서는 모든 SMS 요청에 대해 두 개의 외부 시스템에서 동일하게 요청을 수행할 수 있어야 합니다.

메시지 플랫폼에 요청되는 SMS 유형은 다음과 같습니다.

  • 일반 SMS/LMS
  • 인증 SMS
  • 해외 수신자에 대한 SMS
  • 이미지를 첨부한 MMS

추가로 투입하는 외부 시스템에 대해 모든 SMS 요청을 처리하는 기능을 개발하는 것은 시간이 조금 걸리는 일입니다.
따라서 가장 비즈니스 영향도가 가장 높은 인증 SMS 유형부터 기능을 개발하는 방향으로 결정했습니다.

그다음 차례대로 개발이 완료된 유형에 대해서는 두 개의 외부 시스템으로 트래픽을 분산하여 발송하는 방향으로 결정했습니다.

어떻게 두 개의 외부 시스템에 트래픽을 분산할까요?

메시지 플랫폼에는 Router라는 모듈이 있습니다.
Router 모듈은 요청받은 메시지가 어떤 외부시스템에 발송 요청할지 결정하고 요청을 넘겨주는 역할을 담당합니다.
따라서 Router 모듈에서 외부 시스템 별 트래픽 비율 정보를 알고 있고, 비율에 따라 A와 B 외부 시스템으로 트래픽을 분산하도록 아키텍처를 결정하였습니다.

Router 모듈부터 외부 시스템까지 요청되는 전반적인 과정은 아래와 같습니다.

  • 인증 SMS에 대해서만 Weighted Routing Policy를 사용하여 A 외부 시스템과 B 외부 시스템로 트래픽을 분산할 수 있게 합니다.
  • 일반 SMS, 해외 SMS, MMS는 아직 B 외부 시스템에서 구현이 완료되지 않았으므로 Default Routing Policy를 사용하여 모든 SMS 요청을 처리할 수 있는 A 외부 시스템로만 발송하도록 합니다.

프로젝트 완료 후, 최종적으로 Router 모듈에서 SMS를 처리하는 모습은 아래와 같습니다.

  • 일반 SMS, 해외 SMS, MMS를 순서대로 B 외부 시스템에서 처리할 수 있도록 구현 후, 구현되는 순서대로 Weighted Routing Policy를 사용하도록 합니다.

다음으로 요구되는 내용은 A 외부 시스템와 B 외부 시스템로 원하는 만큼 트래픽을 분산할 수 있어야 합니다.

  • A 외부 시스템 장애 상황 시에는 B 외부 시스템로만 요청이 가도록 해야 합니다.
  • B 외부 시스템 장애 상황 시에는 A 외부 시스템로만 요청이 가도록 해야 합니다.
  • A 외부 시스템에서 처리할 수 있는 동시 처리량을 넘어서는 경우에는 B 외부 시스템에 트래픽을 50:50으로 분산할 수 있어야 합니다.

트래픽 분산 기능은 Weighted Routing Policy에 있는 트래픽 비율 정보를 바탕으로 이루어지게 됩니다.
어떻게 트래픽 비율 정보를 배포 없이 바꿀 수 있을까요?

어떻게 배포 없이 트래픽을 변경할까요?

Spring Boot 애플리케이션에서는 application.properties 혹은 application.yml 파일을 이용해 프로퍼티를 정의하고 아래와 같이 사용할 수 있습니다.

sms:
  systems:
    routing:
      weight:
        A: 51
        B: 49
@Component
class SmsWeightedRoutingPolicy(
    @Value("\${sms.systems.routing.weight.A}") 
    private val systemRoutingWeightForA: Int,
    @Value("\${sms.systems.routing.weight.B}") 
    private val systemRoutingWeightForB: Int,
)

위와 같이 WeightedRoutingPolicy에서 트래픽 비율 알고 있고, 트래픽 비율에 따라 어떤 kafka topic으로 보낼지만 선정하면 발송 이중화를 구현할 수 있게 됩니다.
여기까지 구현이 완료되었다면 배포를 통해 프로퍼티를 다시 적용하여 트래픽 비율을 변경할 수 있게 됩니다.

하지만 저희는 시스템 운영 중에 배포 없이 트래픽을 변경하는 방법이 필요했습니다.
이를 구현하기 위해 Spring Cloud Config를 사용하여 런타임에 프로퍼티 정보를 변경할 수 있는 기술을 도입하게 되었습니다.

Spring Cloud Config를 사용하기 위해 두 가지 아키텍쳐를 고안하여 팀에서 토론을 진행하였습니다.

  1. Spring Cloud Config + Spring Cloud Bus + Spring Cloud Config Monitor를 이용한 아키텍처
  2. Spring Cloud Config + Scheduled Polling을 이용한 아키텍처

두 아키텍처를 소개하고 어떤 과정을 통해 1안과 2안 중 하나를 결정하게 되었는지 살펴보겠습니다.

1안. Spring Cloud Config + Spring Cloud Bus + Spring Cloud Config Monitor를 이용한 아키텍처

공통적으로

  • Spring Cloud Config를 이용하여 Remote Properties를 구성하여 배포 없이 message-platform-router 내의 외부 시스템 별 발송 트래픽 정보를 변경하고 반영하는 시스템을 구성하였습니다.
  • 시스템 적으로 프로퍼티 정보를 바꾸기 용이하게 하기 위해 Spring Cloud Config에서 제공하는 Backend 중 RDB(MySQL)를 활용하는
    JDBC Backend를 사용하게 되었습니다.

Spring Cloud Config를 처음 접하는 분이라면 아래 글을 참고 부탁드립니다.
기술에 대한 상세한 설명이 길어 제가 개인블로그에 정리한 글입니다.

1안으로 제시된 Spring Cloud Config + Spring Cloud Bus + Spring Cloud Config Monitor를 이용한 아키텍처를 살펴보겠습니다.

각 단계에 대한 설명은 아래와 같습니다.

  1. DB에 프로퍼티 데이터가 수정되면 Config Server 애플리케이션에서는 Spring Cloud Config Monitor에서 제공하는 /monitor endpoint를 호출합니다.
  2. /monitor 엔드포인트로 요청된 정보를 바탕으로 kafka에 클라이언트의 프로퍼티 정보를 갱신하도록 하는 RefreshRemoteApplicationEvent를 발행합니다.
  3. 클라이언트는 kafka topic을 구독하고 있고 이벤트를 수신하여 클라이언트의 프로퍼티 정보를 갱신합니다.
  4. 클라이언트를 갱신 시, Config Server에 자신의 application-name과 profile 정보를 기반으로 프로퍼티 정보를 조회합니다.
  5. Config Server는 요청받은 application-name과 profile 정보를 기반으로 DB에 저장된 프로퍼티 정보를 조회하여 API 응답으로 리턴합니다.
  6. 조회된 프로퍼티 정보를 기반으로 클라이언트에 @RefreshScope로 등록된 Bean을 재생성합니다.

2안. Spring Cloud Config + Scheduled Polling을 이용한 아키텍처

다음으로는 Spring Cloud Config 정보를 주기적으로 Polling 하는 스케줄러를 사용한 방식입니다.

Scheduler에 대한 구현은 아래 글을 참고 부탁드립니다.

2안으로 제시된 Spring Cloud Config + Scheduled Polling를 이용한 아키텍처를 살펴보겠습니다.

각 단계에 대한 설명은 아래와 같습니다.

  1. message-platform-router 모듈에서 10초 주기로 Config Server에 application-name과 profile 정보를 기반으로 프로퍼티 정보를 조회합니다.
  2. Config Server는 요청받은 application-name과 profile 정보를 기반으로 DB에 저장된 프로퍼티 정보를 조회하여 API 응답으로 리턴합니다.
  3. 이 아키텍처에서는 state라는 필드를 사용해 state 값이 변경된 경우에만 Context Refresh 하도록 합니다.
    1. state 값이 변경되지 않은 경우에는 Context Refresh를 실행하지 않도록 하여 비효율적으로 Bean을 재생성하는 과정을 제거하였습니다.
    2. state 값에는 가장 최근에 변경된 프로퍼티의 수정시간 timestamp 값을 사용하였습니다.
    3. 응답 정보의 state 값이 변경된 경우에는 조회된 프로퍼티 정보를 기반으로 클라이언트에 @RefreshScope로 등록된 Bean을 재생성합니다.

최종 결정

1안과 2안 모두 장단점이 있는 아키텍처입니다. 각 아키텍처에 대한 장단점을 한번 살펴보겠습니다.

1안. Spring Cloud Config + Spring Cloud Bus + Spring Cloud Config Monitor를 이용한 아키텍처

장점

  • 프로퍼티에 대한 수정이 발생하였을 때 config server의 /monitor 엔드포인트를 호출하여
    클라이언트 별로 1회만 프로퍼티 조회 API를 호출하여 클라이언트에 프로퍼티 변경 사항을 반영합니다.
  • 실시간에 가깝게 반영됩니다.

단점

  • 클라이언트에 이벤트를 전파하기 위해 kafka와 같은 메시지 브로커가 필요합니다.
  • 또한 kafka가 정상적이지 않은 경우 프로퍼티 설정을 변경할 수 없는 구조입니다.

2안. Spring Cloud Config + Scheduled Polling을 이용한 아키텍처

장점

  • Kafka와 같은 메시지 브로커를 관리하는 비용이 없습니다.

단점

  • 클라이언트가 주기적으로 config server에 프로퍼티 조회 API를 요청하는 구조입니다.
  • 클라이언트의 개수가 늘어나게 되면 config server로의 프로퍼티 조회 요청량이 선형적으로 증가하는 구조입니다.
    • 클라이언트의 개수가 작은 경우에는 문제가 되지 않지만, 많아지는 경우 많은 트래픽이 발생합니다.

장단점만 놓고 보면 1안이 상당히 합리적인 아키텍처로 보입니다.
클라이언트 갱신을 위한 API 통신 비용이 저렴하고, Config Server에 부담을 주는 구조가 아니기 때문입니다.

하지만 프로퍼티 설정을 변경하기 위해 Kafka에 의존해야 하는 부분이 선택에 큰 부분으로 작용하였습니다.

  • 1안의 경우 장애가 발생하면 config-server, kafka, config-client에 대해 모두 확인을 해야 합니다.
  • 2안의 경우 장애가 발생하면 config-server, config-client만 확인하면 되고 재시도도 1안에 비해 쉬운 편입니다.

또한 2안의 단점인 config server에 발생하는 지속적인 트래픽도 현재 기준으로는 주기적으로 polling을 하더라도 config sever에 무리가 가는 트래픽이 발생하지 않는다고 판단하였습니다.
요구사항 또한 실시간으로 반영되는 것이 아닌 수 초 내에 변경되는 것으로 목적으로 하고 있어 트레이트 오프를 따졌을 때 운영 비용이 상대적으로 낮은 2안을 선택하게 되었습니다.

위와 같은 선택을 통해 메시지 플랫폼에서는 빠르게 SMS 발송 이중화를 구현할 수 있었습니다.
이후 내부 어드민에 프로퍼티를 변경할 수 있는 기능을 추가하여 외부 시스템 별 트래픽 설정을 빠르게 변경할 수 있어 장애가 발생하더라도 빠르게 대응할 수 있는 시스템을 구성하게 되었습니다.

2021.10.19 다시 놈이 나타났다.

2021.10.19 22:30분경 한 외부 시스템의 장애가 발생하여 외부 시스템으로 요청하는 SMS에 대해 발송이 불가능한 상황이 발생하였습니다.


하지만 저희는 장애를 극복할 수 있는 모든 준비가 되어있었고, 장애를 인지함과 동시에 안정적으로 SMS를 발송할 수 있는 외부 시스템으로 트래픽을 100% 전환하였습니다.😁

더 이상 장애가 발생해도 좌절과 무력함을 겪지 않고 차분하게 대응할 수 있게 되었습니다.
또한 외부 시스템 장애 공유 시에도 자신 있게 장애 공유를 할 수 있었습니다.

아직 끝나지 않은 이중화 여정기

SMS 채널에 대해서는 완벽하게 이중화가 구현되어 SMS 채널 장애에 대해서는 극복할 수 있는 시스템을 갖췄습니다.
4월에는 알림톡 채널에 대해 이중화를 지원하게 됩니다. 가장 핵심적인 채널 2개에 대해 장애 극복력을 갖출 수 있게 되었습니다.

다음 단계로 저희가 해보고자 하는 과제는 자동화입니다.
현재는 알람이나 제보를 통해 사람이 장애를 인지하고 판단하여 트래픽을 전환하고 있습니다. 수동으로 사용하게 된 이유는 장애라고 판단할 수 있는 기준을 정하기 어렵기 때문이었습니다.
하지만 이제는 그간의 운영 경험을 바탕으로 장애라고 판단할 수 있는 몇 가지 기준을 정할 수 있게 되었습니다.
이런 상황에서는 시스템이 자동으로 트래픽을 전환하도록 자동화하는 기능을 개발해 볼 예정입니다.

마치며

메시지 발송 이중화 여정기라는 긴 글을 읽어 주셔서 감사합니다.
이중화를 구성하는 과정에서 새로운 기술도 찾아보고 프로토 타입도 만들어 보며 힘들지만 재밌는 과정을 경험하였습니다.
저희와 같은 고민을 하시는 분들을 위해 글을 공유하면 좋을 것 같아 글을 쓰게 되었습니다.
중간에 있는 Spring Cloud Config에 대한 기술은 내용이 길어 개인 기술 블로그에 자세하게 기술하였습니다. 글을 읽는 과정에서 이해가 되지 않는 부분이 있다면 참고 부탁드리겠습니다.

저희와 함께 이런 고민들을 해결하고 같이 성장하고 싶은 욕구가 있으신 개발자분들은 언제든지 [Tech] 프로덕트부문 공통 플랫폼 개발자 모집에 지원 부탁드리겠습니다.
저희 팀에서 쓴 다른 기술 블로그 포스팅도 궁금하시다면 아래를 참고해보세요

공통시스템개발팀에서 작성한 글 살펴보기

참고