← 목록

사내 통합 포털의 Redis(Valkey) 사용 구조: 권한 캐시와 실시간 알림

Task를 늘리자 생긴 두 가지 문제를 Redis로 푼 구조와, 그 대가로 생긴 의존성을 어떻게 보고 있는지

2026-04-23AWSElastiCacheValkeyRedisECSArchitecture

들어가며

사내 통합 포털에서 Redis(ElastiCache Valkey)는 두 가지 역할을 한다.

  1. 권한 분산 캐시: 서비스 간 권한 정보 공유
  2. 실시간 알림 브로드캐스트: 멀티 인스턴스 간 SSE 알림 전달

둘 다 처음부터 계획했던 게 아니라, Task를 여러 개로 늘리면서 생긴 문제를 풀다가 들어왔다. 한 대로 돌 때는 없던 문제가 여러 대가 되는 순간 생긴다는 점에서 성격이 같다.


1. 권한 분산 캐시

문제 배경

이 포털은 마이크로서비스 구조로, auth-service가 사용자 권한을 관리하고 각 서비스가 그 권한을 기준으로 요청을 처리한다.

ECS Fargate에서 각 서비스가 여러 Task로 확장되면, Task마다 로컬 메모리에 권한 정보를 캐시하게 된다. auth에서 권한을 바꿔도 ALB가 요청을 분산하기 때문에 일부 Task에만 반영되고, 나머지 Task는 예전 권한을 들고 있다.

관리자가 권한 변경
      │
      ├─ Task A: 변경 반영됨   → 요청 성공
      └─ Task B: 예전 권한 유지 → 권한 오류

같은 사용자가 같은 화면에서 새로고침만 해도 결과가 달라진다. 재현이 안 되는 버그로 들어오기 때문에 운영에서 특히 곤란한 종류다.

선택지들

해결 방법은 여럿이었고, 기준은 "권한 변경이 얼마나 빨리 전파되어야 하는가"였다.

방법전파 지연문제
로컬 캐시 유지 + 짧은 TTLTTL만큼지연을 줄이려면 TTL을 줄여야 하고, 그러면 조회가 늘어난다
매번 auth-service 호출즉시auth가 모든 요청의 경로에 들어와 병목이 된다
변경 시 각 Task에 무효화 이벤트 전파즉시Task 목록을 알아야 하고, 스케일 아웃마다 대상이 바뀐다
공유 캐시(Redis) 사용즉시Redis 의존이 생긴다

권한은 바뀌는 즉시 반영되어야 하는 데이터라서 지연을 허용하기 어려웠고, Task가 수시로 바뀌는 Fargate 환경에서는 대상 목록을 관리하는 방식도 맞지 않았다. 그래서 상태를 밖으로 빼는 쪽을 택했다. 컨테이너는 언제든 교체된다는 전제에서는, 공유해야 하는 상태를 컨테이너 안에 두지 않는 편이 단순하다.

동작 흐름

[auth-service]
  │
  │ 1. 관리자가 사용자 권한 변경
  │ 2. auth가 Redis에 권한 정보 저장 (Write)
  │
  ▼
[Redis (ElastiCache Valkey)]
  │
  │  키: permission:service:{serviceCode}:user:{userId}:company:{companyId}
  │  값: ["permission1", "permission2", ...]  (JSON 배열)
  │
  ├──────────────────┬──────────────────┐
  ▼                  ▼                  ▼
[tax-service]   [retail-x]   [automobile]  ...
  Task A           Task A        Task A
  Task B           Task B

  3. 각 서비스가 요청 처리 시 Redis에서 권한 조회 (Read)
  4. 모든 Task가 동일한 최신 권한 데이터를 참조

키에 서비스 코드, 사용자, 회사를 모두 넣은 이유는 같은 사용자가 회사와 서비스에 따라 다른 권한을 갖기 때문이다. 키 설계가 곧 무효화 단위가 된다. 회사 단위로 권한을 갈아엎어야 할 때 어떤 키들을 지워야 하는지가 이 구조에서 정해진다.

코드 구조

auth-service와 각 서비스가 동일한 RedisConfig를 갖는다.

환경별 접속 구성

환경접속 방식인증
local/devStandalone개발 서버 직접 접속
stgCluster (TLS)Secrets Manager에서 환경변수로 주입
prodCluster (TLS)Secrets Manager에서 환경변수로 주입

STG와 Prod는 rediss://(TLS)와 AUTH Token을 함께 쓴다. 이 두 계층이 각각 무엇을 막는지는 ElastiCache 접속 보안 구조에 정리했다.

커넥션 풀 설정

환경auth Master/Slave기타 서비스 Master/Slave비고
STG48 / 4832 / 32auth가 쓰기도 하므로 더 크다
Prod64 / 6464 / 64

인프라 관점에서 이 숫자는 Task 수와 함께 봐야 한다. 실제 연결 수는 (풀 크기) × (Task 수) × (서비스 수) 로 늘어나기 때문에, 스케일 아웃을 크게 잡아두면 캐시 쪽 연결 수가 먼저 한계에 닿을 수 있다. 애플리케이션 설정 하나가 인프라 용량 계획에 들어오는 지점이다.


2. 실시간 알림 브로드캐스트 (auth-service 전용)

문제 배경

사용자는 브라우저에서 SSE(Server-Sent Events)로 실시간 알림을 받는다. Task가 여러 개면 사용자 A는 Task 1에, 사용자 B는 Task 2에 연결된다.

알림이 Task 1에서 생성됐는데 사용자 B가 Task 2에 붙어 있으면, Task 1은 사용자 B에게 직접 보낼 방법이 없다. 연결은 특정 Task에 묶여 있는데 이벤트는 아무 Task에서나 발생하기 때문이다.

해결: Redis Pub/Sub (RTopic)

모든 Task가 같은 토픽을 구독하게 해서, 어느 Task에서 발생한 알림이든 전체에 뿌린다.

[알림 발생]
  │
  ▼
[auth-service Task 1]
  │
  │ 1. notificationTopic.publishAsync(메시지)
  │    → Redis RTopic "sse:notification"에 publish
  │
  ▼
[Redis Pub/Sub]
  │
  │ 2. 같은 토픽을 구독 중인 모든 Task가 메시지 수신
  │
  ├─────────────────────┐
  ▼                     ▼
[Task 1]              [Task 2]
  │                     │
  │ 3. 로컬 emitter에서  │ 3. 로컬 emitter에서
  │    해당 userId 찾아   │    해당 userId 찾아
  │    SSE push          │    SSE push
  ▼                     ▼
[사용자 A 브라우저]    [사용자 B 브라우저]

각 Task는 자기에게 연결된 사용자만 골라 보낸다. 브로드캐스트는 Redis가 하고, 필터링은 각 Task가 한다.

코드 핵심

이 방식의 성질

Pub/Sub은 보관하지 않는다. 메시지가 발행되는 순간 구독 중인 쪽에만 전달되고, 그 뒤에 들어온 구독자는 받지 못한다.

Task 재배포 중 (구독 끊김)
  → 이 사이에 발행된 알림은 그 Task에 연결된 사용자에게 전달되지 않는다
  → 나중에 복구돼도 지난 메시지는 오지 않는다

알림이 유실돼도 사용자가 화면을 새로 열면 목록에서 확인할 수 있는 구조라 지금은 이 성질을 받아들이고 있다. 만약 유실이 허용되지 않는 알림이 생긴다면 Pub/Sub이 아니라 저장되는 방식(Redis Streams나 별도 큐)으로 옮겨야 한다. 이 판단 기준을 적어두는 것이, 나중에 요구사항이 바뀌었을 때 다시 조사하지 않아도 되게 해준다.


정리

역할사용 서비스Redis 기능설명
권한 캐시auth (쓰기), 기타 서비스 (읽기)Key-Value 저장서비스 간 권한 공유, 스케일 아웃 시 일관성 보장
실시간 알림auth 전용Pub/Sub (RTopic)멀티 인스턴스 간 SSE 알림 브로드캐스트

인프라 관점 주의사항

단일 의존 지점에 대해

이 구조의 대가는 분명하다. 권한 조회 경로에 Redis가 들어왔기 때문에, Redis가 흔들리면 로그인 이후의 거의 모든 요청이 영향을 받는다. 실제로 AUTH TOKEN 로테이션 미완료 장애 때 이 구조 때문에 서비스가 기동조차 못 했다.

그래서 이 의존을 줄일지, 받아들이고 관리할지를 정해야 한다. 지금은 후자를 택하고 있고, 그 전제에서 할 일은 이렇다.

무엇이 죽으면 무엇이 멈추는지를 그려두는 것이 구조 문서의 핵심이라고 생각한다. 그 그림이 있어야 장애 때 확인할 순서가 정해진다. 실제 대응 절차는 사내 통합 포털 운영 가이드에 있다.


배운 점

한 대에서 여러 대가 되는 순간 새로운 문제가 생긴다. 권한 불일치도, SSE 알림 전달도 Task가 하나일 때는 없던 문제였다. 스케일 아웃은 용량을 늘리는 작업처럼 보이지만, 실제로는 공유되어야 할 상태가 무엇인지 묻는 작업이었다.

상태를 밖으로 빼면 일관성을 얻고 의존성을 내준다. Redis를 쓰기로 한 순간 권한 전파 문제는 사라졌지만, 대신 서비스 전체가 캐시 하나에 묶였다. 어느 쪽이 나은지는 상황에 따라 다르고, 중요한 건 무엇을 내줬는지 알고 있는 것이다.

저장되지 않는 것은 미리 적어둬야 한다. Pub/Sub 유실은 평소에 드러나지 않다가 재배포나 노드 재시작 때 나타난다. 이런 성질은 겪고 나서 알기보다, 구조를 정리할 때 적어두는 편이 싸다. 같은 이유로 SES CloudTrail 데이터 이벤트에서도 기록되지 않는 경로를 문서에 남겼다.


이어 읽기