프로젝트/레트로 테트리스

Redis 제거와 복구코드 재발급 기능 구현 기록 (TDD + NestJS 트러블슈팅)

ksc-dev 2026. 6. 15. 20:51

이번 작업은 단순 기능 추가가 아니라, 인프라 구조 변경과 비즈니스 로직 개선이 함께 이루어진 작업이었다.

크게 두 가지 흐름으로 정리된다.

  • Redis 제거 및 NestJS 부팅 에러 트러블슈팅
  • 복구코드 재발급 기능 구현 (TDD 기반)

1. Redis 제거 과정에서 발생한 ioredis ENOTFOUND 에러

문제 상황

테트리스 랭킹 조회 페이지에서 다음 에러가 발생했다.

[ioredis] Unhandled error event: Error: getaddrinfo ENOTFOUND flexible-hen-82321.upstash.io
 

이상한 점은 이미 Redis 캐시 코드는 제거한 상태였다는 것이다.

  • score.service.ts에서 redis 호출 제거
  • 캐시 로직은 더 이상 사용하지 않음

그런데도 애플리케이션은 Redis 연결을 시도하고 있었다.


원인 분석: “호출 제거”와 “모듈 제거”는 다르다

문제의 핵심은 NestJS 구조에 있었다.

RedisModule은 여전히 AppModule과 ScoreModule에 import 되어 있었다.

AppModule → RedisModule
ScoreModule → RedisModule
 

RedisService 내부의 숨은 부작용

RedisService에는 다음 코드가 있었다.

 
async onModuleInit() {
    this.client = new Redis(process.env.REDIS_URL ?? 'redis://localhost:6379')
}
 

ioredis는 생성 시점에 바로 연결을 시도한다.

즉:

모듈이 살아있는 순간 Redis 연결도 같이 시작된다


결과

  • 코드 호출 제거 ❌
  • RedisModule 유지 ❌
  • onModuleInit 실행 ❌
  • Upstash 연결 실패 ❌

→ ENOTFOUND 에러 발생


추가 문제: RateLimitGuard의 숨은 Redis 의존성

캐시는 제거했지만 RateLimitGuard는 여전히 Redis를 사용하고 있었다.

 
redis.incr()
redis.expire()
 

즉 구조는 이렇게 되어 있었다:

  • 캐시 ❌
  • 레이트리밋 ❌ (여전히 Redis 의존)

선택지

1) Redis 유지

  • Upstash 재발급
  • REDIS_URL 교체

2) Redis 제거

현재 상황:

  • 트래픽 낮음
  • 캐시 사용 없음
  • 운영 단순화 필요

결론:

Redis 제거가 더 합리적


Redis 제거 작업

1. 모듈 제거

  • AppModule / ScoreModule → RedisModule 제거

2. 서비스 제거

  • RedisService DI 제거
  • 캐시 코드 제거

3. Redis 폴더 삭제

  • module
  • service
  • test

4. 테스트 변경

  • Redis mock 기반 테스트 → DB 기반 테스트로 변경

5. RateLimitGuard 변경

Redis 기반 → 메모리 기반 Fixed Window 방식

 
type Bucket = { count: number; resetAt: number }

private readonly buckets = new Map<string, Bucket>()
 

트레이드오프

장점

  • 외부 Redis 제거
  • 배포 단순화
  • 비용 감소
  • 장애 포인트 감소

한계

  • 멀티 인스턴스 환경에서 공유 불가
  • 서버 확장 시 rate limit 정확도 깨짐

핵심 교훈

“코드를 지웠다”와 “의존성을 제거했다”는 완전히 다르다

NestJS에서는 특히 다음이 중요하다:

  • Module imports
  • Provider lifecycle
  • onModuleInit side effect

2. 복구코드 재발급 기능 구현 (TDD 적용)

Redis 제거 작업과 함께 복구코드 재발급 기능도 개선했다.


요구사항

복구코드를 재발급하되 기존 기록은 유지해야 한다


초기 문제

처음 구현은 단순했다.

  • 새 코드 생성
  • 쿠키만 교체

하지만 문제가 발생했다.


문제 상황

DB에는 복구코드 hash 기준으로 데이터가 저장되어 있었기 때문에:

  • 코드만 변경 → 기존 기록 연결 끊김
  • 결과적으로 데이터가 사라진 것처럼 보이는 문제 발생

해결 방향: 데이터 마이그레이션

복구코드 변경 시 기존 데이터를 함께 이동시키는 구조로 변경했다.


핵심 구현 (AuthService.resetCode)

 
async resetCode(oldCode: string): Promise<string> {
    const newCode = this.generateCode()

    await this.prismaService.score.updateMany({
        where: { recoveringCode: hashCode(oldCode) },
        data: { recoveringCode: hashCode(newCode) }
    })

    return newCode
}
 

구조 설명

  • 기존 hash → 새로운 hash로 일괄 업데이트
  • Prisma updateMany 활용
  • HMAC-SHA256 기반 hash 구조 유지

API

PATCH /auth/reset
 

흐름

  1. 쿠키에서 oldCode 추출
  2. resetCode 호출
  3. DB updateMany 실행
  4. newCode 쿠키 저장

TDD 적용

RED → GREEN → REFACTOR 방식으로 진행


테스트 케이스

  • 쿠키 없음 → BadRequestException
  • 쿠키 있음 → 코드 재발급 + 데이터 유지 확인

결과

✔ 8 / 8 테스트 통과


3. 전체 흐름

클릭
 → PATCH /auth/reset
 → DB: oldHash → newHash updateMany
 → 쿠키 교체
 → UI 상태 업데이트
 

4. 결론

이번 작업에서 핵심은 단순 기능 구현이 아니라 구조적인 문제 해결이었다.

  • Redis 제거로 인한 부팅 에러 해결
  • NestJS 모듈 구조 이해
  • RateLimitGuard 재설계
  • 복구코드 데이터 무결성 유지
  • TDD 기반 기능 구현

5. 실제 적용: Markdown 기반 SDD 문서

이번 복구코드 기능에서 SDD 방식이 적용되었다.

예를 들어 복구코드 기능은 다음처럼 정리되었다.


- SDD 적용 과정

docs/features/recovering_code.md

처음 작성한 요구사항은 다음과 같았다.

(초안 내용) 
### 기능 추가 구현
복구코드 재발급 기능을 백엔드 및 프론트엔드 각 부분에 TDD로 검증해서 구현해줘(작업 순서: TDD -> 구현 -> 리펙토링)

## API
- PATCH /reset

# TDD(API)
- 존재하지 않는 복구코드를 재발급하는 경우
- 존재하는 복구코드를 재발급하는 경우
- 중복된 부분이 많으면 리펙토링 필수

## WEB
- 버튼 스타일 : 레트로 게임 느낌(선택적)
- hover / active 상태 존재
- 클릭시 loading되고 서버에서 처리가 되면 loading 풀리게끔

# TDD(WEB)
- 마우스 대면 hover 상태인가?
- 클릭하면 active 상태이고 loading 중인가?

 

하지만 실제 구현 과정에서 다음 문제가 발견되었다.

- 테스트 기준이 모호함
- 데이터 유지 조건이 누락됨

그래서 요구사항을 다음과 같이 수정했다.

 

(최종 요구사항)

### 기능 추가 구현
1. 복구코드 재발급 기능을 백엔드 및 프론트엔드 각 부분에 TDD로 검증해서 구현해줘(작업 순서: TDD -> 구현 -> 리펙토링)
2. TDD는 작성 후 각 단계에 실행해서 확인해야 해.(RED -> GREEN -> REFACTORING) 
3. 복구코드 재발급 기능을 복구코드를 재발급하되 내 기록들을 유지해야 해

## API
- PATCH /reset

# TDD(API)
- 존재하지 않는 복구코드를 재발급하는 경우
- 존재하는 복구코드를 재발급하는 경우
- 중복된 부분이 많으면 리펙토링 필수

## WEB
- 버튼 스타일 : 레트로 게임 느낌(선택적)
- hover / active 상태 존재
- 클릭시 loading되고 서버에서 처리가 되면 loading 풀리게끔

# TDD(WEB)
- 마우스 대면 hover 상태인가?
- 클릭하면 active 상태이고 loading 중인가?

 

최종 정리

결국 이번 작업을 통해 느낀 핵심은 하나였다.

기능을 “끄는 것”과 시스템에서 “완전히 제거하는 것”은 다르다

 

그리고 기능 개발에서 더 중요한 것은 

코드 자체가 아니라, 데이터 흐름과 구조를 이해하는 것이다.

 

🔗 직접 플레이해보기

이번 글에서 정리한 내용은 실제 서비스에 반영되어 있습니다.
아래 링크에서 확인할 수 있습니다.

 

https://tetris-retro-web.vercel.app/

 

tetris_retro_web

 

tetris-retro-web.vercel.app