📌 고정 게시글

📢 공지합니다

이 게시글은 메인 페이지에 항상 고정되어 표시됩니다.

최코딩의 개발

[붐빔] MSA와 RabbitMQ로 알림 API 응답 성능 개선하기 본문

개발 팁

[붐빔] MSA와 RabbitMQ로 알림 API 응답 성능 개선하기

seung_ho_choi.s 2025. 8. 31. 18:37
728x90

프로젝트 소개

안녕하세요! 스위프트 앱2기에서 진행하고 있는 실시간 인구 공유 플랫폼 Boombim을 개발하고 있습니다. 초기에는 모놀리식 아키텍처로 시작했지만, 서비스 규모가 커지는걸 고려해 생각해본 결과 알림 기능의 성능 이슈가 심각해졌습니다.

 

특히 FCM 알림 발송 시 18초라는 말도 안 되는 응답 시간을 기록하면서 뭔가 대책이 필요하다는 걸 깨달았습니다.

 

그래서 MSA 아키텍처와 RabbitMQ를 도입해서 성능을 개선해보기로 했습니다. 결과적으로 126ms까지 응답 시간을 단축시킬 수 있었는데, 그 과정에서 겪었던 시행착오와 해결 방법들을 공유해보려고 합니다.

MSA 아키텍처란 뭘까?

https://haon.blog/static/12f095b7174e91730c0d0eeac6b1cf2b/e4611/image.png

모놀리식 vs MSA 아키텍처

먼저 기본부터 정리해보겠습니다.

모놀리식 아키텍처는 하나의 큰 애플리케이션에 모든 기능이 통합되어 있는 구조입니다. 하나의 통합된 단위로 애플리케이션을 구축하는 접근 방식이죠. 마치 모든 기능이 하나의 거대한 건물 안에 들어있는 것과 같습니다.

 

반면 MSA(Microservices Architecture)는 비즈니스 역량을 중심으로 조직된 느슨하게 결합된, 배포/실행 가능한 컴포넌트들의 집합으로 애플리케이션을 구조화하는 방식입니다. 쉽게 말해서 큰 건물을 여러 개의 작은 건물들로 나누어 각각 독립적으로 운영하는 것과 비슷합니다.

MSA의 장단점

장점:

  • 독립적 배포: 각 서비스를 개별적으로 배포할 수 있어서 전체 시스템을 내릴 필요가 없습니다
  • 기술 다양성: 서비스마다 다른 기술 스택을 사용할 수 있습니다
  • 확장성: 개별 서비스들이 모듈로 분리되어 있어서 동시에 여러 서비스에 확장 명령을 전송할 수 있습니다
  • 장애 격리: 한 서비스에 문제가 생겨도 다른 서비스에는 영향을 주지 않습니다

단점:

  • 복잡성 증가: 모놀리식 설계보다 더 많은 구성 요소들이 움직이기 때문에 초기 복잡성이 증가합니다
  • 네트워크 통신 오버헤드: 서비스 간 통신이 네트워크를 통해 이루어지므로 지연시간이 발생할 수 있습니다
  • 데이터 일관성: 분산 환경에서 데이터 일관성을 유지하는 게 어렵습니다
  • 운영 복잡도: 모니터링, 로깅, 디버깅이 복잡해집니다

그래서 MSA의 핵심은 도메인 간 의존성을 최대한 줄이는 것이 중요합니다. 잘못 설계하면 오히려 독이 될 수 있습니다.

첫 번째 시도: 3개 서버로 분리

 

처음에는 붐빔 서비스를 위와 같이 3개로 나눴습니다.

  1. API 서버: 일반적인 비즈니스 로직 처리
  2. 장소 스케줄링 및 분석 서버: 서울 실시간 인구 데이터 공식 장소 관련 스케줄링 및 데이터 처리
  3. RabbitMQ 브로커 & 컨슈머 서버: 알림 전송 전담

도메인 의존성 문제 발생

하지만 곧 문제에 직면했습니다. 우리 도메인 구조를 보면:

  • Member (사용자)
  • Alarm (알림)
  • Vote (투표)
  • Place (장소)
  • FCM (푸시 알림)
  • 등등

RabbitMQ 서버에서는 FCM 도메인만으로 알림을 전송하고 싶었는데, 알림이 전송되면 DB에 저장되어야 하고, 이를 위해서는 Alarm 도메인과 Member 도메인이 함께 필요했습니다.

 

이렇게 되면 API 서버에서 Member와 Alarm 도메인을 업데이트할 때마다 RabbitMQ 서버에서도 같이 업데이트해야 하는 상황이 발생했어요. 오히려 더 비효율적이 되었습니다.

해결 시도와 한계

해결책으로 RabbitMQ 서버에 Alarm, FCM 도메인을 묶어서 처리하고, API 서버에서는 알림 내역 조회 시 DTO와 Feign Client를 활용해서 어느 정도 분리를 시도해봤습니다.

하지만 Member 도메인이 문제였습니다. 이 도메인이 거미줄처럼 모든 곳에 얽혀있어서 쉽게 분리할 수 없었습니다. Member 관련해서도 별도 서버를 만들어야 할 것 같았지만, 현재 시간과 비용 제약으로 인해 현실적으로 불가능했습니다.

현실적 타협: 물리적 분리로 부하 분산

결국 현실적인 타협안을 선택했습니다. API 서버, 장소 스케줄링 및 분석 서버, RabbitMQ 서버 이렇게 3개로 물리적으로 나누어서 요청 부하를 분산시키는 것으로 결정했습니다.

 

완벽한 MSA는 아니지만, 적어도 각 서버가 담당하는 역할을 명확히 분리하여 성능 향상을 꾀할 수 있었습니다.

 

RabbitMQ 도입 배경과 이유

RabbitMQ란?

https://www.cloudamqp.com/img/blog/exchanges-topic-fanout-direct.png

 

RabbitMQ는 메시지 브로커(Message Broker) 역할을 하는 오픈소스 소프트웨어입니다. 애플리케이션 간에 메시지를 주고받을 수 있게 해주는 중간 매개체 역할을 합니다.

 

장점:

  • 비동기 처리: 요청과 응답을 분리하여 성능 향상
  • 확장성: 메시지 큐를 통한 효율적인 작업 분산
  • 안정성: 메시지 영속성 보장
  • 유연성: 다양한 패턴(publish/subscribe, work queue 등) 지원

단점:

  • 복잡성: 추가적인 인프라 관리 필요
  • 네트워크 의존: 네트워크 장애 시 전체 시스템 영향
  • 메시지 순서: 순서 보장이 복잡할 수 있음

이거는 추후 다시 다루겠습니다. 

우리 서비스의 알림 기능

붐빔에는 총 3가지 알림 기능이 있습니다:

  1. 공지 알림: 관리자가 보내는 공지사항
  2. 투표 종료 알림: 투표가 마감되었을 때 알림
  3. 정기 알림: 매일 오후 4시에 소통방 참여 독려 알림

기존 FCM 구현의 문제점

기존에는 공지 API가 호출되면 다음과 같은 흐름으로 처리했습니다:

  1. FCM 활성화 토큰 조회
  2. 각 사용자에게 개별적으로 FCM 전송

1,000명을 임의로 만들어서 테스트한 결과 이 방식의 문제는 순차적으로 처리하다 보니 응답 시간이 무려 18,000ms(18초)나 걸렸다는 것입니다. 사용자 경험상 말이 안 되는 수준이었습니다.

첫 번째 개선: 배치 처리

 

개별 전송을 병렬 처리로 바꾸고, 500개씩 배치로 묶어서 처리하도록 개선했습니다. 그 결과 4,067ms로 단축되었습니다. 하지만 여전히 만족스럽지 않았고, N+1 문제도 많이 발생했습니다.

RabbitMQ 도입 후 아키텍처

새로운 설계 사상

사용자 조회 관련 코드와 알림 발송하는 코드를 완전히 분리하고, 이를 호출하는 별도의 시스템이 있으면 좋겠다는 생각이 들었습니다. 그래서 메시큐를 도입했고 그 중 RabbitMQ를 도입하게 되었습니다.

개선된 알림 처리 흐름

공지 알림 예시:

  1. API 서버: 알림 엔티티 생성 → 상태를 '큐 대기'로 설정 → Producer로 메시지 전송
  2. RabbitMQ 브로커: 메시지 큐잉 및 라우팅
  3. Consumer 서버: 큐에서 메시지 하나씩 처리 → 배치로 묶어서 FCM 알림 발송

성과

이렇게 2개로 나누어 처리하니까:

  • API 서버: API 처리에만 집중 가능
  • RabbitMQ 서버: 알림 발송에만 집중 가능
  • 응답 시간: 18,000ms → 126ms로 99.3% 단축

물론 RabbitMQ 자체가 비동기 방식이라서 API 응답시간만 측정한 것이고, 실제 알림 전송 완료 시간은 배치 방식과 크게 다르지 않을 겁니다. 하지만 전체적인 코드 흐름과 관심사 분리 측면에서는 확실히 개선되었습니다.

아키텍처 다이어그램

도전과 한계

도메인 의존성 문제

MSA를 도입하면서 가장 큰 어려움은 도메인 간 의존성이었습니다. 특히 Member 도메인이 모든 곳에 얽혀있어서 완전한 분리가 어려웠어요. 이상적으로는 Member 서비스도 별도로 분리해야 하지만, 현실적인 제약으로 인해 타협할 수밖에 없었습니다.

현실적 제약

  • 시간 부족: 완벽한 MSA 구조로 리팩토링할 시간이 부족했습니다
  • 비용 문제: 추가 서버 운영에 따른 인프라 비용 부담
  • 팀 규모: 작은 팀에서 복잡한 MSA를 관리하기에는 리소스가 제한적이었습니다.

RabbitMQ 도입 상세

알림 처리 성능 이슈

기존 FCM 구현 방식의 문제점:

  • 순차 처리: 사용자 한 명씩 순서대로 알림 전송
  • N+1 쿼리: 사용자 조회 시 비효율적인 쿼리 발생
  • 동기 처리: API 응답이 모든 알림 전송 완료까지 기다려야 함

개선 전략

  1. 비동기 처리: API 응답과 알림 전송을 분리
  2. 배치 처리: 500개씩 묶어서 효율적으로 처리
  3. 큐 시스템: 안정적인 메시지 전달 보장

구현 세부사항

Producer (API 서버):

// 알림 엔티티 생성
Alarm alarm = Alarm.builder()
    .message(message)
    .status(AlarmStatus.QUEUED)
    .build();
    
// RabbitMQ로 메시지 전송
rabbitTemplate.convertAndSend(
    "notification.exchange", 
    "notification.key", 
    alarm.getId()
);

Consumer (RabbitMQ 서버):

@RabbitListener(queues = "notification.queue")
public void processNotification(Long alarmId) {
    // 배치 단위로 FCM 토큰 조회
    List<String> tokens = fcmService.getActiveTokensBatch(alarmId);
    
    // 500개씩 나누어 병렬 처리
    tokens.parallelStream()
        .collect(Collectors.groupingBy(it -> tokens.indexOf(it) / 500))
        .values()
        .parallelStream()
        .forEach(batch -> fcmService.sendBatch(batch, message));
}

성능 개선 결과

구분 개인별 FCM 전송 500건 단위 배치 처리 MSA 기반 RabbitMQ 비동기 처리
처리 구조 단일 서버 내 개별 요청 순차 처리 단일 서버 내 일정 단위로 병렬 전송 API 서버(Producer) ↔ 알림 서버(Consumer) 분리, 메시지 큐 기반
처리 방식 동기(Synchronous) 병렬(Partial Parallel) 완전 비동기(Asynchronous)
응답 지연 시간 18,000 ms 4,067 ms 126 ms
개선율(이전 단계 대비) - 77.4 % 단축 97 % 추가 단축
서버 부하 매우 높음 개선됨 (중간 수준) 최소화됨
확장성 낮음 보통 높음 (대규모 트래픽 안정 처리 가능)
특징 요약 요청 단위로 전송되어 지연 발생 일정 단위 병합으로 속도 개선 MSA 및 큐 분산 처리로 실시간 성능 확보

 

1,000명을 임의로 테스트 한 결과 약 99.3%의 성능 향상을 달성했습니다!

 

이전에도 말씀드렸듯이 API 응답시간만 측정한 것이고, 실제 알림 전송 완료 시간은 배치 방식과 크게 다르지 않을 겁니다. 하지만 전체적인 코드 흐름과 관심사 분리 측면에서는 확실히 개선되었습니다!

교훈과 앞으로의 계획

배운 점

  1. 완벽한 MSA보다는 점진적 개선: 처음부터 완벽한 구조를 만들려고 하지 말고, 현실적 제약 안에서 개선 가능한 부분부터 시작하는 것이 중요하다는것을 느꼈습니다. 
  2. 도메인 분리의 어려움: Member 같은 핵심 도메인은 생각보다 분리하기 어렵습니다. 설계 초기부터 이를 고려해야 된다는것을 느꼈습니다. 
  3. 성능 vs 복잡성: MSA는 성능 향상을 가져다주지만 운영 복잡도도 함께 증가한다는 점을 명심해야 합니다

향후 개선 계획

  1. Member 서비스 분리: 시간과 리소스가 확보되면 Member 도메인을 별도 서비스로 분리
  2. API Gateway 도입: 서비스 간 통신을 더 체계적으로 관리
  3. 모니터링 강화: 분산 환경에서의 로깅과 모니터링 체계 구축
  4. Database per Service: 각 서비스별 독립적인 데이터베이스 구성

마무리

스위프트 앱2기 붐빔 프로젝트에서 MSA와 RabbitMQ를 도입하면서 많은 것을 배웠습니다. 완벽한 구조는 아니지만, 현실적 제약 안에서 최대한의 성능 개선을 이뤄낼 수 있었습니다..

 

특히 18초에서 126ms로 알림 성능을 개선한 것은 사용자 경험 관점에서 엄청난 발전이었습니다. 앞으로도 지속적으로 아키텍처를 개선해나가면서 더 나은 서비스를 만들어가려고 합니다.

 

MSA 도입을 고민하고 계신 분들께는 "완벽함보다는 점진적 개선"을 추천드리고 싶습니다. 현실적 제약을 인정하면서도 개선 가능한 부분부터 차근차근 시작하는 것이 중요한 것 같습니다.

728x90