낙서장이자 오답 노트이자 컨닝 페이퍼

패스트캠퍼스 환급챌린지 30일차 : 코드팩토리의 백엔드 아카데미 : 한 번에 끝내는 NestJS 패키지 - 기초부터 MSA까지 강의 후기 본문

NestJS

패스트캠퍼스 환급챌린지 30일차 : 코드팩토리의 백엔드 아카데미 : 한 번에 끝내는 NestJS 패키지 - 기초부터 MSA까지 강의 후기

NangIn 2025. 4. 30. 22:50

본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.

 

강의 내용 정리   

 

Access Token 재발급하기

 

목적

  • Access Token은 유효기간이 짧음 (보안 목적)
  • Refresh Token을 통해 Access Token 재발급 가능
  • → 로그인 없이 세션 유지

 

엔드포인트 구성

@Post('token/access')
rotateAccessToken(@Headers('Authorization') token: string) {
  const payload = await this.authService.parseBearerToken(token, true);
  return {
    accessToken: await this.authService.issueToken(payload, false),
  };
}
  • Authorization: Bearer <refresh_token> 형식으로 요청
  • refresh_token 유효성 검증 후, 새로운 access_token 발급

 

토큰 파싱 함수 분리

  • parseBearerToken()
    • async parseBearerToken(rawToken: string, isRefresh: boolean): Promise<{ id: number; role: Role; }> {
        const [type, token] = rawToken.split(' ');
        if (type.toLowerCase() !== 'bearer') throw new BadRequestException('포맷이 잘못됐습니다.');
      
        const payload = await this.jwtService.verifyAsync(token, {
          secret: this.configService.get(isRefresh ? 'REFRESH_TOKEN_SECRET' : 'ACCESS_TOKEN_SECRET'),
        });
      
        // 토큰 타입 체크
        if (isRefresh && payload.type !== 'refresh') {
          throw new BadRequestException('Refresh Token을 입력해주세요.');
        }
        if (!isRefresh && payload.type !== 'access') {
          throw new BadRequestException('Access Token을 입력해주세요.');
        }
      
        return { id: payload.sub, role: payload.role };
      }
      ​
       
      • Bearer 포맷 확인
      • verifyAsync()로 서명/만료 검증
      • payload의 type이 'refresh'인지 확인 필수

 

토큰 발급 함수 개선

  • issueToken() 수정
    • async issueToken(payload: { id: number; role: Role }, isRefresh: boolean): Promise<string> {
        return this.jwtService.signAsync(
          {
            sub: payload.id,
            role: payload.role,
            type: isRefresh ? 'refresh' : 'access',
          },
          {
            secret: this.configService.get(
              isRefresh ? 'REFRESH_TOKEN_SECRET' : 'ACCESS_TOKEN_SECRET',
            ),
            expiresIn: isRefresh ? '24h' : 300,
          },
        );
      }
      ​
       
      • payload.sub, payload.role, type 명시
      • expiresIn은 refresh → 24h, access → 5분(300초)

 

환경변수 정비

 

왜 환경 변수를 상수로 분리해야 하는가?

  • .env의 키들을 하드코딩된 문자열로 직접 반복 사용하고 있었음
  • 이는 오타 발생 시 런타임 에러로 이어질 수 있음
  • 변수화해 관리하면 오타를 줄이고 유지보수가 쉬움

 

공통 상수 관리 폴더 및 파일 생성

  • 구조:
    • src/common/const/env.const.ts
  • 코드:
    • export const EnvVariableKeys = {
        DB_TYPE: 'DB_TYPE',
        DB_HOST: 'DB_HOST',
        DB_PORT: 'DB_PORT',
        DB_USERNAME: 'DB_USERNAME',
        DB_PASSWORD: 'DB_PASSWORD',
        DB_DATABASE: 'DB_DATABASE',
        HASH_ROUNDS: 'HASH_ROUNDS',
        ACCESS_TOKEN_SECRET: 'ACCESS_TOKEN_SECRET',
        REFRESH_TOKEN_SECRET: 'REFRESH_TOKEN_SECRET',
      };
      

 

사용처 리팩토링

  • app.module.ts
    • ConfigModule.forRoot({
        validationSchema: Joi.object({
          [EnvVariableKeys.DB_TYPE]: Joi.string().required(),
          [EnvVariableKeys.DB_HOST]: Joi.string().required(),
          ...
        })
      })
      
  • auth.service.ts
    • this.configService.get(EnvVariableKeys.ACCESS_TOKEN_SECRET)
      this.configService.get(EnvVariableKeys.REFRESH_TOKEN_SECRET)
      this.configService.get(EnvVariableKeys.HASH_ROUNDS)
      

 

정상 작동 여부 테스트

  • 기존 API (/auth/login, /auth/token/access) 테스트 → 정상 작동
  • 실수로 access token을 refresh token 엔드포인트에 넣으면 → 에러 응답

 

refresh token 만료 시 에러 처리 개선

  • 기존 문제:
    • 만료된 refresh token 입력 시, 내부 에러만 발생 → 사용자에게 불명확한 응답
  • 개선 방법:
    • try {
        const payload = await this.parseBearerToken(token, true);
        ...
      } catch (err) {
        throw new UnauthorizedException('토큰이 만료됐습니다.');
      }
      
    • try-catch로 감싸고, 명시적인 메시지를 가진 예외를 던짐

 

학습 후기         

 

이번 강의를 통해 Access Token과 Refresh Token을 함께 사용하는 인증 구조의 실용성을 체감할 수 있었습니다. 기존에는 토큰이 만료되면 무조건 로그인을 다시 해야 한다고 막연히 생각했지만, Refresh Token을 통해 Access Token만 교체해 주는 구조는 실제 서비스에서 매우 합리적인 방식이라는 점을 알게 되었습니다. 사용자 경험을 해치지 않으면서도 보안을 유지할 수 있다는 점이 특히 인상 깊었습니다.

 

특히 parseBearerToken 메서드로 Access와 Refresh 토큰을 구분하고, issueToken 메서드로 두 토큰을 모두 발급할 수 있게 설계한 구조는 단순하면서도 확장성이 뛰어나다고 느꼈습니다. 이 과정에서 토큰 타입을 payload.type으로 구분하는 아이디어는 향후 로그아웃이나 토큰 블랙리스트 기능을 구현할 때도 유용할 것 같습니다.

 

추가로 .env 환경 변수 키를 상수화하여 관리하는 방식은 실무에서 반드시 필요한 설계 패턴이라는 인사이트를 얻었습니다. 문자열을 반복해서 사용하는 대신 상수 객체로 관리하니 오타를 방지할 수 있었고, ConfigService를 사용하는 모든 부분에서 일관된 접근 방식이 가능해졌습니다. 이는 코드의 안정성과 유지보수성을 크게 높여주는 작업이었습니다.

 

또한, Refresh Token이 만료되었을 때 내부 에러 메시지로 끝나는 것이 아닌, UnauthorizedException을 통해 명확한 응답 메시지를 클라이언트에 전달하는 처리 방식은 사용자 친화적인 API 설계의 좋은 예시라고 느꼈습니다. 단순히 작동하는 코드보다, 클라이언트와의 소통을 고려한 예외 메시지 설계가 얼마나 중요한지를 다시금 느낄 수 있었습니다.

 

이번 학습을 통해 인증 구조를 보다 체계적이고 견고하게 구성하는 방법을 익혔으며, 무엇보다도 실무 환경에서 반복적으로 마주치게 될 인증 문제들을 사전에 대비할 수 있는 탄탄한 설계 철학을 배울 수 있었습니다. Access Token 재발급 기능은 단순한 기능 이상으로 사용자 경험, 보안, 유지보수 측면에서 모두 중요한 역할을 한다는 점에서 앞으로도 적극적으로 활용하고 싶은 기술 중 하나가 되었습니다.

 

학습 인증샷       

                       

수강 인증 사진
학습 인증샷
공부 시작 시간
공부 종료 시간

https://abit.ly/lisbva