
들어가며
요즘 유명한 기술 스택을 적용해보면서 자주 드는 생각은, "왜 이 기술을 선택해야 했는가"를 설명할 수 있어야 한다고 생각한다. 이번 글에서는, 6개월전 나를 열받게 만들었던 한 상황을 꺼내며 Redis를 선택할 수밖에 없었던 감정적 이유 + 기술적 이유를 함께 이야기해보려고 한다.
나는 예전에 인프런을 보며 JWT 기반 로그인은 구현해봤지만, 소셜 로그인 + Refresh Token을 제대로 구현해본 적은 없었다. 그런데 지난 프로젝트에서 팀원분이 소셜로그인을 구현하시면서 Redis 기반 Refresh Token 저장소가 필요해지는 상황이 발생했다.
문제는… 그 당시 나는 나의 첫 무중단 배포(CI/CD)를 성공시키려고 혈안이 된 상태였고, 어찌저찌 배포는 되게 만들어놨지만 조금만 건들면 무너지는 구조였다.
그 환경 위에 새 포트를 열고 Redis를 서버에 붙이는 작업은 솔직히 그때의 내 역량으로는 어떻게 해야하는지 떠오르지도 못했다. 더 큰 문제는 따로 있었다. 프론트 분들도 API 연동을 해야 하는데 소통 부족으로 그대로 merge되면서 모든 API가 Access Token을 필수로 요구하는 상태가 되어버렸고, 결국 프론트는 Swagger조차 사용하지 못하는 상황이 됐다.
회의 결과 빠르게 Redis를 붙이는 쪽으로 방향이 정해졌지만, 다른 중요한 백엔드 로직도 merge된 상태라 되돌릴 시간이 없었다. 자신이 없었는지 내가 만든 부분을 밤새워 고치겠다는 말을 팀원들에게 건네지 못하였다. 결국 팀장님이 “제가 예전에 구축해둔 Docker 기반 배포 환경이 있는데, IAM 계정만 주시면 금방 붙일 수 있어요.” 라고 도와주셨고, 내가 2주일 동안 애써서 만든 배포 환경은 그날부로 완전히 교체되었다.
처음엔 당당하게 CI/CD를 자원했고, 늦어지더라도 팀원들의 응원을 받으며 어떻게든 완성했는데, 결국 내 능력 부족으로 결과적으로 팀에 폐를 끼친 느낌이 들었다. 억울하기도 하고, 열받기도 했다.
그때 팀장님이 Redis와 도커 환경을 순식간에 붙이는 걸 보면서 “와… 나도 저렇게 해야 하는데…” 라는 감정이 처음 들었고,
그 이후로 아래 Docker를 공부하기도 했다. (지금 도커 기반 배포는 자신있다!)
https://basalt-scribe-35f.notion.site/Docker-240f3b5f6008806dbd92c069439012e8?source=copy_link
Docker 공부 | Notion
프로젝트 진행 중 백엔드 팀장님이 docker 환경으로 배포 환경 구축 후 Redis 같은 추가적인 기능 추가할 때 너무 편하게 추가하신 것을 보고 감명받아 이름만 들어본 도커 공부해려고 한다!
basalt-scribe-35f.notion.site
결론은 아래다,,
아니 그래서, 내 첫 CI/CD를 없애버린 Redis가 대체 뭔데?!
Refresh Token은 Access Token을 갱신해주는 건 알겠는데
거기서 왜 Redis가 필요한데?!
지금도 그때의 장면을 떠올리면 많이 열받는다. 그래서 이번 프로젝트에서는 나를 그렇게 분노하게 만들었던 Redis를 정면으로 이해하고, 직접 적용해보기로 했다.
본문으로
💢 Redis가 대체 뭔데?
검색해서 글을 읽어보니 “Redis는 인메모리 기반 NoSQL Key-Value 저장소입니다.” 란다. 그리고 캐시기능, 인메모리여서 빠르다는 말이 정말 많다. 와닿지 않는다. 왜 빠를까?
✔ RDBMS 흐름
- 디스크(I/O) → 페이지 캐시 → 버퍼 풀 → 쿼리 파서 → 옵티마이저 → 실행 계획 → 결과 반환
- 결국 느릴 수밖에 없다.
✔ Redis 흐름
- 인메모리에서 바로 값 읽기
- 문자열·리스트·셋·해시·ZSet 같은 자료구조 기반으로 즉시 계산
- 결과 반환
중간 과정이 없다. 캐싱이 아니라 “애초에 인메모리 DB로 설계된 것”이 핵심이다.
→ 결국 단일 read가 1ms 미만이다. Redis의 속도는 단순히 “인메모리라서 빠르다”가 아니라
“I/O 계층을 통째로 스킵한다” 는 아키텍처적인 장점 때문이다.
🤔 그럼 Redis를 "언제" "왜" 쓰는걸까?
Redis는 RDBMS처럼 ACID(원자성.일관성.독립성.지속성)를 보장하지 않는다. 엥? 그럼 왜쓰지..?(처음에 든 생각이다)
RDBMS는 디스크 기반 저장소이고, 데이터의 영속성과 정합성을 최우선으로 보장한다. 우리가 흔히 생각하는 “트랜잭션 = 믿음직한 처리”라는 인식은 바로 이 RDBMS의 특성에서 나온다.
그런데 Redis는 철학 자체가 다르다.
Redis의 핵심 목표는 데이터를 “영원히 안전하게 저장하는 것”이 아니라,
엄청 빠르게 읽고 쓰면서, 짧은 시간 동안 유지되는 상태(Session/Cache/Token/TTL)를 관리하는 것이다.
즉 Refresh Token, 비밀번호 재시도 카운트, 하루 단위로 갱신되는 Seed 값처럼 “언제든 날아가도 상관없지만 반드시 빨라야 하는 데이터” 를 Redis에 넣는 것이 가장 합리적이다.
또한 디스크 DB에는 TTL(Time To Live)이라는 개념이 없다. “이 데이터는 24시간 뒤에 사라져야 한다”는 요구사항을 구현하려면:
- 스케줄러 만들기
- 만료일 컬럼 만들기
- 삭제 쿼리 작성
- 혹은 배치 잡 실행
즉 TTL이 걸린 데이터 하나 삭제하려고 서비스 코드가 복잡해진다. Redis는?
SET key value EX 86400
한 줄 끝이다. wow!
이 TTL 기반 데이터 생명주기 제어는 Refresh Token, 오늘의 운세 Seed, 이메일 인증코드, SMS OTP 등에서 DB보다 훨씬 단순하고, 의도에 맞고, 장애 위험이 낮다.
Redis는 캐시 서버가 아니라 "시간에 민감한 데이터의 생명주기를 관리하는 메모리 기반 상태 서버”라고 표현하고 싶다.
그래서인지 다음 표현이 확 와닿지 않는가?! “핫 데이터는 Redis로 올려 Fast Path로 처리하고, 변하지 않는 데이터는 DB로 두어 Slow Path로 둔다.”
🧐 Redis가 분산 환경에서 진가가 드러난다?!
서버가 2대 이상이 되면 Access Token 인증, Refresh Token 저장, Seed 값 등이 서버 간 공유되어야 한다. Redis는 분산 환경에서 이런 문제를 해결한다:
- 모든 서버가 같은 Redis를 보며 동일한 상태 공유
- 한쪽 서버가 재시작되어도 데이터 유지
- 세션을 DB에 저장할 필요 없음
- 스케일아웃 시에도 Seed/Token 일관성 유지
즉 Redis는 서버 수가 늘어나면 필수가 되는 인프라 구성 요소이다.
이번 프로젝트도 단일 서버지만, 소셜 로그인을 구현하다보니 Refresh Token을 같이 도입했다. 이제는 Refresh Token은 RDBMS에 넣기엔 과하다고 느껴질 정도로 Redis의 특성과 완벽하게 맞아떨어지는 데이터라고 당당하게 말할 수 있지 않겠는가!
열받았던 과거 시점에서의 redis 도입이유는 아래와 같이 다시 정리해볼 수 있을 것이다.
카카오 로그인으로 얻은 토큰을 그대로 인증 토큰으로 계속 쓰며 다른 api를 호출하기엔 비용이나 성능상의 문제가 있을테니 자연스럽게 우리만의 Access Token이 필요했고, 보안상의 이유로 Access Token의 만료기간을 짧게 해야했을 것이다. 계속 재로그인을 시키기엔 UX가 너무 불편하니 당연하게 Refresh Token이 필요해지고.. 그러면서 Redis가 도입됐던 것이구나...를 이제야 깨닫는다...!
🛠️ 내가 적용한 부분
이제 개념적인 Redis 이야기를 넘어서, 내 서비스에서 Redis가 실제로 어떻게 동작했는지를 설명해보겠다.(사실 열받아서 꼭 적용해보고 싶었다.)
여기서는 두 가지 흐름을 다뤘다.
- Refresh Token 관리 (소셜 로그인 후 자체 인증 유지)
- 오늘의 운세 API (하루 한 번만 바뀌는 Seed 캐싱)
두 기능은 다르지만, 결국 “짧은 생명주기 + 빠른 조회 + 상태 관리”라는 공통된 성격 때문에 Redis에 넣는 것이 가장 합리적이었다.
🧑💻 Refresh Token 관리 — 나의 서비스의 인증 상태를 Redis에 저장하기
이번에 도전한 나의 첫 소셜 로그인은 구글 로그인이다. (GPT가 구현난이도가 가장 높다고 해서 해봤다🔥) 전체적인 로그인 흐름은 아래와 같다.
[사용자]
│
▼
[Android 앱: Google Sign-In SDK]
│
▼
(1) 사용자가 Google 계정 선택 → Google 서버에 로그인 요청
│
▼
(2) Google이 ID Token 발급 (JWT 형식의 id_token)
│
▼
(3) Android 앱이 이 ID Token을 우리 백엔드에 전송
│
▼
───────────────────────────────────────
[Spring Boot 서버 @ GCP]
───────────────────────────────────────
├─ (4) Google ID Token 검증
│ - Google public key로 서명 검증
│ - iss / aud / exp 필드 검증
│ - 이메일, sub(고유 ID) 추출
│
├─ (5) Member 조회
│ - DB에 해당 Google ID 회원이 있으면 로그인
│ - 없으면 신규 회원으로 저장
│
├─ (6) 우리 서비스 전용 Access Token 발급
│ - 짧은 수명: 인증용
│
├─ (7) 우리 서비스 전용 Refresh Token 발급
│ - 긴 수명: 재발급용
│
└─ (8) Refresh Token을 Redis에 저장 (TTL 설정)
───────────────────────────────────────
│
▼
(9) 앱은 Access Token을 Authorization 헤더에 실어 API 호출
로그인은 위의 흐름으로 진행되게끔 구성했고, Refresh Token을 저장할 때 Redis가 필요한 이유는 위에서 많이 설명했던 것처럼 아래와 같이 정리해볼 수 있겠다.
- TTL 필요
- 빠른 read/write
- 유실돼도 재로그인하면 됨
- “상태 기반 세션 관리”는 Redis의 주력 분야
- 서버 여러 대일 때 공유 가능
🔮 오늘의 운세 API — “24시간 동안 고정 Seed를 유지하는 캐시 서버 역할”
이 api는 간단하게 설명하면 오늘의 운세를 보여주며 24시간뒤에 바뀌어야 하는 api이다. 앱의 주제인 "로또"의 성격과 어울리기에 구현해봤다!!!
운세 시스템의 요구사항은 다음과 같았다:
- 운세는 하루에 한 번만 바뀐다.
- 당일 모든 사용자는 같은 Seed를 사용한다.
- 하지만 유저별 운세 결과는 달라야 한다.
- 트래픽이 몰려도 빠르게 응답해야 한다.(빠르게 읽어오자! Fast Path!!)
- Seed는 24시간 후 자동으로 초기화되어야 한다.
이 요구사항은 모두 Redis의 특성과 절묘하게 맞아떨어진다. 정확히 어떻게 저장하고 썻는지 알아보자
“오늘 날짜 기준으로 단 하나의 Seed를 저장한다.”
2025-11-15 → seed = 17
Redis 저장방식은 아래와 같다.
KEY : fortune:seed:2025-11-15
VALUE : 17
TTL : 86400초 (24시간)
그래서 코드에선 아래와 같이 실행중이다
seed = redis.get("fortune:seed:2025-11-15")
if (seed == null) {
seed = generateRandomSeed()
redis.set(key, seed, Duration.ofDays(1))
}
이렇게 되면 아래와 같은 흐름으로 Seed를 관리할 수 있다.
- 첫 사용자 → Seed 생성
- TTL 만료 → Seed 재생성
- 하루 동안은 동일 Seed 유지
그럼 Seed 같은데 어떻게 멤버별로 다른 운세를 보여줬지?
간단하게.. 아래처럼 구현했다!!
// 동일 Seed + 다른 memberId → 개인화된 운세
index = (memberId + seed) % messageCount
만약 수천 건에 호출되는 랜덤 조회를 DB에 보낸다면 병목의 원인이 되었을 것이다.
그래서 성능 향상, TTL 기능 추가 , 분산 서버 유지가능, 유실되어도 문제없는 데이터 관리를 이루었다고 말하고 싶다!!
🙃 Redis를 배포하며 겪었던 문제
매번 aws로만 배포를 해보아서 이번에 GCP, 즉 구글 클라우드로 배포를 해보았다! 무려 40달러가 무료이다.
그래서 GCP VM에서 Docker + Spring Boot + Redis 구조로 배포했는데… 서버가 계속 죽었다.
로그는 처음부터 아래와 같았다.
RedisConnectionFailureException: Connection refused
그래서 Redis 관련 설정 환경변수 매핑 문제인 줄 알고 컨테이너 내부에 환경변수 export해주고, 컨테이너가 실행되면서 주입되게끔 하는 alias 설정도 해주며 블로그에서 하라는 linux 명령어 다 해봤다... 그런데 해결이 되지 않았다 😂(도커 이미지 새로 만들고.. 푸쉬하고.. 기다리고 정말정말 답답했다.)
근데 문제는 아주 단순했다.
“Redis는 기본적으로 외부 접속(127.0.0.1밖)을 막는다.”
네? ㅎㅎ
Spring Boot는 Docker 안에서 돌아가고, Redis는 VM 호스트에서 돌게끔 설정했었다... 그래야 다른 도커 컨테이너를 띄워도 redis 값을 공유할 수 있을테니....!
그런데 결론은 컨테이너 입장에서 Redis 접근은 외부 접속이므로 차단되었던 것이다.
그래서 꼭 Redis 설정파일 /etc/redis/redis.conf 수정하며 아래와 같은 설정해줘야한다.
// bind 수정
bind 0.0.0.0
// protected-mode 해제
protected-mode no
// Redis 재시작
sudo systemctl restart redis
// Docker Compose 재기동
docker compose down
docker compose up -d
redis의 기본 설정으로 인해 배포에도 정말 많은 시간을 써먹어서... 쉽지 않았다 😂
마무리하며
이번 글은 단순히 Redis를 정리한 기술 포스팅이 아니라, (열받았던) 과거의 고민들과 그 사이에서 성장한 나 자신을 되돌아보는 기록이다..! 6개월 전에는 “Redis가 왜 필요한지” 제대로 이해하지 못해 배포 환경 하나를 지키지 못했던 순간도 있었지만, 지금은 데이터의 성격을 보고 기술을 선택할 수 있는 기준이 생겼다고 말하고 싶다!
그리고 그 기준이 “왜 이 기술을 택했는가?”, “무엇을 해결하려는가?”라는 질문과 정확히 맞닿아 있다고 생각한다.
Refresh Token 관리, TTL 기반 캐싱, 하루 단위 Seed 전략처럼 겉으로는 간단한 기능도 그 뒤에는 여러 설계 선택이 숨어있다는 사실을
이번 설계를 통해 더욱 명확하게 체감했다.
지금도 부족하지만 이렇게 한 걸음씩 문제를 이해하며 설계를 고민하고 기술의 ‘이유’를 설명할 수 있는 개발자가 되기 위해 계속 도전하고 있다. 우아한테크코스 도전이라는 긴 여정 속에서 이번 시도는 분명히 나에게 또 하나의 분기점이 되었다!! 앞으로도 이런 과정들을 꾸준히 기록하며 “문제를 해결하기 위해 적절한 기술을 도입하는 개발자”로 성장해가고 싶다.
'DB' 카테고리의 다른 글
| [PostgreSQL] 처음 써보는 PostgreSQL 알아보기(MySQL과 비교하며) (1) | 2025.11.21 |
|---|