GPU 서버 관리 자동화 시스템 알림 병목 해결기 (소비자-생산자 패턴, Redis Message Queue 알아보기)

2026. 2. 9. 16:20·경험 모음집
반응형
저는 1년간 교내 서버 관리자로 근무하면서 GPU 서버 관리 자동화 시스템의 백엔드를 개발해왔습니다. 
오늘은 이때 마주했던 문제를 해결하는 방법,
그리고 생산자(Producer)-소비자(Consumer) 패턴에 대해 작성해보려고 합니다!

 

 

요구사항

먼저 이해를 돕기 위해 비즈니스 요구사항을 살펴보겠습니다. 
  • GPU 서버의 각 컨테이너(정확히는 Pod)에는 만료 기한이 있습니다. 사람이 직접 수작업으로 컨테이너를 내리는 대신, 정해진 만료 기한에 따라서 컨테이너가 자동으로 정리되어야 합니다. 
  • GPU 서버 관리 자동화 시스템에는 웹 UI가 존재합니다. 각 사용자는 웹 UI에 대한 계정이 있으며, 계정 또한 만료 기한이 존재합니다. 
  • 웹 UI 계정별로 컨테이너는 여러개 생성될 수 있습니다. 앞서 말했던 것처럼 여러개의 컨테이너에는 여러개의 만료기한이 각각 존재합니다.
  • 사용자 알림은 Slack과 Email로 제공됩니다. (웹에서 제공하는 알림은 아님)

 

요구사항을 만족하기 위해, 저는 다음과 같이 프로젝트를 구성했습니다. 

 

A. 스케줄러

  • 스케줄러를 사용해 매일 아침 8시(업무 시작 시간 전)에 만료 기한을 확인하는 동작이 수행됩니다. 
  • 만료 기한이 지난 계정/컨테이너라면 각각 아래의 작업 수행합니다. 
    • 1. 계정: 만료된 계정은 soft-delete를 합니다. (이후 처리 방법은 생략)
    • 2. 컨테이너:
      • 2-1. 만료된 컨테이너는 인프라 서버에 컨테이너 정리 및 Ubuntu 계정 삭제 요청을 보냅니다. 
      • 2-2. 삭제 요청이 수행되면, MySQL로 관리되는 UID/GID 회수 작업을 수행합니다. 

 

B. 알림 전송

Slack 및 Email 알림은 다음과 같은 기준으로 전송됩니다. 

  • 1. 계정: 계정 삭제 7, 3, 1일 전 및 완료 시 사용자 알림 전송
  • 2. 컨테이너: 컨테이너 삭제 7, 3, 1일 전 및 완료 시 사용자 알림 전송
  • 3. 관리자 알림: 작업 완료 및 오류 발생 시 관리자에게 Slack 알림 전송

 

여러 개의 디테일한 작업이 수행되다보니, 처음 구현했을 당시에는 에러가 이곳저곳 튀어나왔습니다. 

왜냐하면 스프링 내부에서 작업을 온전히 처리하는 것이 아니라, Slack API와 저희 팀에서 개발했던 외부 인프라 API까지 호출해야 하는 상황이었기 때문입니다. 따라서 어느 구간에서 에러가 터질 지 확신할 수가 없었습니다. 

 

특히 제가 주목했던 부분은 Slack API를 호출하고 다음 작업을 수행하는 부분입니다. 

  • Slack API에는 종류별로 정도는 다르지만 Rate Limit(속도 제한)이 존재합니다.
  • 짧은 시간 내에 너무 많은 요청이 들어오면 `429 Too Many Requests` 에러를 뱉습니다. 
  • 이 에러가 발생하면 이후에 예정되어있던 작업들이 모두 멈추게 됩니다. 
예를 들어, 사용자 1, 2, 3의 컨테이너를 정리해야 한다고 가정합니다. 
2번에서 429에러가 발생하면, 2번의 컨테이너는 정리된 채로 알림만 가지 않습니다. 
3번의 컨테이너는 정리되지 않고 알림도 가지 않습니다. 

 

이렇게 하나의 스케줄러였지만 하나의 작업 실패가 전체 루프를 중단시킨다는 문제가 있었습니다. 

각 사용자별 작업이 원자성을 가져야 하지만 보장되지 않고 있었습니다. 

 

Redis를 완충 지대로 활용하기

여러가지 방법이 있지만, 저는 기존에 구성되어있던 Redis를 활용했습니다. 

Redis를 단순 저장소가 아닌 메시지 큐로 활용하는 방법이 있는데,

`LPUSH` / `RPOP` / `BRPOP`을 사용하면 순서가 보장되는 큐로 사용할 수 있습니다. 

 

🔎 Redis 명령어 정리하기
- LPUSH: 큐의 왼쪽(Head)에 데이터를 넣습니다. 여기에서는 알림 메시지를 생성하는 생산자가 사용합니다.
- RPOP: 큐의 오른쪽(Tail)에서 데이터를 꺼냅니다. 여기에서는 소비자가 사용하며, First-In-First-Out 구조가 만들어집니다.
- BRPOP: RPOP의 Blocking 버전입니다. 큐에 데이터가 없으면 설정한 시간동안 대기하다가 데이터가 들어오는 즉시 꺼내옵니다. 반복적인 Polling으로 인한 리소스 낭비를 줄일 수 있습니다. 

 

생산자(Producer)와 소비자(Consumer)

생산자(Producer)는 큐에 작업을 쌓고, 소비자(Consumer)는 자신의 처리 능력에 맞춰 작업을 가져옵니다. 

이를 통해 시스템 간 결합도록 낮추고(Decoupling), 갑작스러운 트래픽 폭주(Burst)를 완충할 수 있습니다. 

  • 생산자(Producer): 알림이 발생하면 직접 API를 호출하지 않고 Redis List에 데이터를 넣기만 합니다. (Push)
  • 큐 (Queue) 전송해야 할 알림 데이터를 보관합니다. (Redis)
  • 소비자(Consumer): 1초마다 큐에서 하나씩 빼서 Slack API로 전송합니다. (Pop)
  • ➡️ 이 구조를 통해 속도조절 & 비결합(계정 삭제 비즈니스 로직과 알림 발송 로직이 분리됨)이라는 두 가지 장점을 챙길 수 있었습니다. 

 

🗂️ Redis외에 메시지 큐를 구현하는 옵션들
- RabbitMQ, Apache Kafka, AWS SQS, In-Memory Queue (Java Blocking Queue) 등이 있습니다. 
- 위 프로젝트에서는 RabbitMQ, Kafka를 적용하면 오버엔지니어링이라고 생각했습니다.
- SQS의 경우 온프레미스 환경에서 관리하고 있었기 때문에 제외했습니다.
- Java BlockingQueue는 간단하지만 당연하게도 애플리케이션이 종료되면 큐에 담긴 데이터가 사라집니다. 

 

 

💡 만약 Redis 없이 멀티스레드로만 해결했다면?

1) 만약 뮤텍스 or 세마포어를 활용했다면

  • Slack API의 속도를 제한하기 위해 Semaphore를 사용했다면, 스케줄러 스레드 자체가 대기 상태에 빠질 것입니다.
  • 알림 전송 스레드가 10개로 제한되어있는데, 1000개의 알림이 몰린다면 나머지 990개 요청은 세마포어 permit을 받기 위해 줄을 서서 기다려야 합니다. 
  • 결과적으로, 알림 전송이 끝날 때까지 스케줄러 스레드가 종료되지 못하고 리소스를 점유하게 됩니다. 

 

2) JVM의 BlockingQueue에 알림을 쌓아두고 멀티스레드로 소비한다면

  • 알림을 보내는 도중에 어떠한 이슈로 서버가 재시작되거나 장애가 발생하면, 메모리에 쌓여있던 알림 데이터가 그대로 사라집니다. 
  • 그래서 시스템 관점에서 Redis를 사용했고, 덕분에 알림 데이터를 보관할 수 있습니다. 

 

3) 멀티 스레드 방식에는 해당 서버 내부에서만 유효하기 때문에

  • Redis를 큐로 사용하면 서버가 몇 대가 되든 하나의 큐를 공유합니다. 
  • 따라서 전역적인 속도 제어가 가능해집니다. 
  • 물론 아예 다른 Slack API 키를 사용하면 block 당하는 것을 막을 수도 있다고 생각합니다. 
    • 하지만 서버가 늘어남에 따라 키도 계속 늘어나면 관리하기 복잡해지는 이슈가!
    • 서버마다 다른 키를 쓰면 어떤 서버에서 알림이 나갔는지 추적하기가 어렵습니다. 
    • Slack App 설정이 서버 수만큼 늘어납니다. 
    • 워크스페이스의 앱 설치 개수 제한 등...

 

로컬 멀티스레딩 원리를 분산 환경으로 확장해보기
설명 Index Key word 여기에서는?
1 Bounded Buffer (유한 버퍼) Redis List -> 메모리가 제한된 거대한 전역 공유 버퍼
2 Mutual Exclusion (상호 배제) Redis의 Single-Thread 원자성
3 Race Condition (경합 상태) LPUSH / RPOP -> 데이터가 들어오고 나가는 순서 보장
4 Condition Sync (wait/notify) Worker의 fixedDelay & BRPOP -> 큐 상태에 따른 유연한 작업 처리
5 Busy Waiting 방지 Scheduling -> 무의미한 루프 대신 시스템 자원을 효율적으로 관리

 

1) 로컬 메모리가 아닌 외부 저장소를 선택하면서, 서버가 꺼져도 데이터가 유실되지 않는 지속성을 확보합니다. 

 

2) Java의 synchronized는 단일 서버 내에서만 동작합니다. Redis는 Single-Thread라는 특성을 가지고 있는데, 모든 명령어를 순차적으로 처리하기 때문에 수많은 생산자(서버)가 동시에 알림을 넣어도 데이터 꼬임 없이 안전하게 저장합니다. 

 

 3) 여러 스케줄러가 동시에 돌아갈 때 알림 순서가 뒤바뀌거나 중복 전송될 위험이 있습니다. Redis의 LPUSH / RPOP은 원자적 연산이기 때문에, 먼저 들어온 알림이 먼저 나가는 FIFO구조를 보장합니다. 

 

4) Java의 `wait` / `notify`는 할 일이 없으면 스레드를 재우고(wait), 일이 들어오면 깨웁니다(signal). 이를 네트워크 기반으로 확장하면 Worker의 `fixedDelay`, Redis의 `Pop`을 조합합니다.

  • `fixedDelay`: Spring의 `fixedDelay`를 활용해 소비 주기를 1초로 제한 (Slack Rate Limit 회피용)
  • `BRPOP`(Blocking Right POP): 큐가 비어있을 때 무의미하게 응답을 받는 대신, 데이터가 들어올 때까지 Redis가 연결을 붙잡고 기다려줍니다. (정해진 timeout까지만!) 데이터가 들어오는 즉시 워커를 깨워 처리합니다. 

5) 큐가 찼는지 / 비었는지 계속 확인하는 Busy Waiting대신, Spring의 스케줄러를 사용해 정해진 주기에만 워커가 작동합니다. 알림이 없는 시간대에는 CPU를 거의 점유하지 않습니다.

 

🚧조금 더 챙겨볼 디테일

1. TransactionalEventListener활용

  • 일반 `@EventLister`도 있고, `@TransactionalEventListener`도 있습니다. 
    • 일반 `@EventLister`: `publishEvent()`가 호출되는 즉시 실행.
    • `@TransactionalEventListener`: Publisher의 트랜잭션 상태에 따라 이벤트 소비 시점(phase)을 조절.
    • 만약 계정 삭제 중간에 이벤트를 발행했는데, 이후에 어떤 이후로 DB 작업이 롤백된다면? -> DB 데이터는 복구되었지만, 사용자에게는 이미 '계정 삭제'알림이 전송되는 불일치 현상이 발생합니다. 따라서 `@TransactionalEventListener`로 소비 시점을 조절하였습니다. 
  • Phase 설정하기: `AFTER_COMMIT`을 활용했습니다. 
    • DB에 계정 삭제가 실제로 성공한 후에만 알림을 보내야 하고, DB 작업은 실패했는데 알림만 가면 안 되기 때문입니다. 
    • 즉 '이벤트 발행 시점'과 '실제 알림 발송 시점'이 분리됩니다. 
    • 계정 삭제 로직이 포함된 DB 트랜잭션이 최종 커밋되기 전까지는 알림이 큐에 담기지 않습니다. 이를 통해서 무결성을 더욱 강화할 수 있었습니다. 

 

 

⚠️ 다른 옵션과 `AFTER_COMMIT` 비교하기

`@TransactionalEventListener`에는 다양한 phase 옵션이 있습니다.

BEFORE_COMMIT 트랜잭션이 커밋되기 직전에 실행 커밋 전에 반드시 수행되어야 하는 부가 로직이 있을 때 사용합니다.
AFTER_COMMIT 트랜잭션이 성공적으로 완료된 후 실행 DB 작업은 롤백되었는데 알림만 발송되는 '데이터 불일치'를 원천적으로 차단합니다.
AFTER_ROLLBACK 트랜잭션이 실패했을 때 실행 실패 시 로그를 남기거나 복구 작업이 필요할 때 사용합니다.
AFTER_COMPLETION 성공/실패 여부와 상관없이 실행 작업 완료 후 리소스를 정리할 때 사용합니다.

 

 

2. Fallback로직 

  • AlarmService.pushToQueue에서 Redis가 죽어버렸을 때 직접 전송으로 전환합니다. 
  • 메시지 큐 자체가 장애가 났을 때 시스템이 마비되지 않도록 방어적으로 설계했습니다. 

 

3. 비동기 처리

  • 기존 방식 (동기/Blocking): 스케줄러가 알림을 보낼 때마다 Slack API의 응답이 올 때까지 기다려야 했습니다. 만약 전송해야 할 알림이 100개이고 각각 0.5초가 걸린다면, 스케줄러는 알림 전송에만 50초를 소모하게 됩니다.
  • 개선 후 (비동기/Non-blocking): 이제 스케줄러는 Redis 큐에 데이터를 '던지기만' 하고 바로 다음 유저의 삭제 로직으로 넘어갑니다. 큐에 넣는 작업은 밀리초(ms) 단위로 끝나기 때문에, 전체 스케줄러 수행 시간이 단축될 수 있었습니다. 

 

💬 앞으로 더 수정한다면?

Worker의 Retry 전략

  • 현재 코드에서는 catch 블록에서 로그만 남기고 있습니다. 
  • 간단하게는 시스템 에러 시 메시지를 유실하지 않기 위해 큐에 다시 집어넣는 rightPush 재시도 로직을 추가하면 좋겠습니다.
    • 만약에 코드가 잘못되어 발생하는 에러일 경우에는 무한루프를 돌 수 있기 때문에 retry 횟수를 지정해야 할 것 같습니다. 
  • 네트워크 일시 오류 시 재시도 로직이나, 끝까지 실패한 메시지를 보관하는 DLQ(Dead Letter Queue)를 추가적으로 구현하면 안정성이 더 강화될 수 있다는 생각이 들었습니다.
    • 재시도를 여러번 했는데도 계속 실패하는 메시지를 처리하면 좋겠습니다. 
    • 메인 큐와 죽은 메시지 큐를 따로 분리해두고 재시도 횟수가 넘은 메시지들을 보관합니다. 
      • 이를 통해 문제있는 메시지가 메인 시스템의 흐름을 방해하지 않습니다. 
      • 나중에 개발자가 DLQ를 확인해서 디버깅을 하거나, 쌓인 메시지를 수동으로 재발송할 수 있습니다. 

 

반응형

'경험 모음집' 카테고리의 다른 글

우리팀 아기를 위한 - Github 히스토리 기반 인수인계 봇 만들기 with Spring AI  (1) 2026.01.17
1년간의 대학교 GPU 서버 관리자 회고...무언가 돈을 받고 한다는 것  (2) 2026.01.15
AWS RDS(MySQL) Migration하기 (📦 새 계정으로 이사가요)  (0) 2025.11.11
🏅제 6회 KDT 해커톤 최우수상(고용노동부장관상) 후기 - 구름톤 딥다이브부터 듀오블룸까지  (4) 2024.12.17
'경험 모음집' 카테고리의 다른 글
  • 우리팀 아기를 위한 - Github 히스토리 기반 인수인계 봇 만들기 with Spring AI
  • 1년간의 대학교 GPU 서버 관리자 회고...무언가 돈을 받고 한다는 것
  • AWS RDS(MySQL) Migration하기 (📦 새 계정으로 이사가요)
  • 🏅제 6회 KDT 해커톤 최우수상(고용노동부장관상) 후기 - 구름톤 딥다이브부터 듀오블룸까지
kiritoni
kiritoni
안녕하세요, cool & soft한 백엔드 개발자가 되고싶은 토니입니다!
    반응형
  • kiritoni
    Code Art Online
    kiritoni
  • 전체
    오늘
    어제
    • 분류 전체보기 (32)
      • 경험 모음집 (5)
      • Spring Boot (9)
      • Java (0)
      • JPA (0)
      • Server (13)
      • CS (5)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    nlb
    JPA
    gdgoc
    ubuntu
    springSecurity
    CS
    java
    로드밸런서
    network
    서버
    구름톤
    Spring
    백준
    보안
    해커톤
    pfsense
    알고리즘
    구름톤딥다이브
    docker
    Spring boot
    backend
    빅챗
    AUSG
    웹
    kdt
    springboot
    Linux
    server
    be
    고용노동부
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
kiritoni
GPU 서버 관리 자동화 시스템 알림 병목 해결기 (소비자-생산자 패턴, Redis Message Queue 알아보기)
상단으로

티스토리툴바