본문 바로가기
프로젝트

Access Token 재발급: Refresh Token의 역할

by Jiyoung Oh 2025. 5. 13.

 

💬 왜 토큰 재발급이 필요할까?

JWT는 인증 정보를 담고 있는 토큰으로, 발급된 후에는 서버가 별도로 상태를 저장하지 않아도 되는 stateless(무상태) 인증 방식이다. 이 방식은 서버 확장성 면에서 유리하지만, 단점도 있다. 바로 이 stateless 구조 때문에 Access Token이 만료되면 서버 입장에서 이를 갱신할 방법이 없다는 것이다.

Access Token이 만료될 때마다 사용자가 다시 로그인을 진행한다면 사용자 경험이 나빠지게 된다. 그렇다면 사용자가 로그인한 상태를 유지하려면 어떻게 해야 할까? 그래서 등장하는 개념이 바로 Refresh Token이다.

이번 글에서는 Refresh Token을 이용하여 Access Token을 재발급하는 방법에 대해 알아보고, 이번 프로젝트에서 어떻게 적용했는지 공유하고자 한다.


JWT 기반 인증 구조

1. 로그인 시

  • 서버는 Access Token (짧은 수명)과 Refresh Token (긴 수명)을 발급
  • Access Token은 클라이언트가 매 요청마다 Authorization 헤더에 담아 보냄
  • Refresh Token은 HttpOnly 쿠키로 클라이언트에 저장 (자바스크립트에서 접근할 수 없게 하여 탈취 방지)

2. Access Token 만료 시

  • 프론트엔드는 401 응답을 감지하고 /api/auth/reissue API로 쿠키에 담긴 Refresh Token을 이용해 재발급 요청
  • 서버는 Refresh Token이 유효한지 검증한 뒤, 문제가 없다면 새로운 Access Token을 발급
  • 이후 클라이언트는 새로운 토큰으로 요청을 재시도

3. 보안을 강화하기 위해 Refresh Token Rotation 적용

  • 재발급 시 Refresh Token도 새로 발급하고 기존 것을 폐기
  • 탈취된 Refresh Token 재사용 방지
  • 매 재발급 시 서버에서 저장된 값을 갱신하기 때문에 최신 토큰 외엔 모두 무효
  • Rotation을 적용하면 보안성은 높아지지만, 매 요청 시 Refresh Token을 교체하고 DB 업데이트가 필요하므로 성능 고려 필요

프로젝트 구현

이번 프로젝트에서는 사용자의 인증 상태를 안정적으로 유지하고, 보안적인 측면도 고려한 인증 흐름을 제공하기 위해 JWT 기반의 Access Token 재발급 기능을 설계하고 구현했다. 이를 위해 Refresh Token을 활용한 토큰 재발급 구조를 도입하고, 서버에서 Refresh Token의 유효성을 검증한 후 새로운 Access Token을 발급하는 흐름을 구성하였다. 이 과정에서 Refresh Token의 회전(Rotation) 방식과 HttpOnly 쿠키, 토큰 저장 및 비교를 위한 DB 관리 등 인증 설계 요소들을 적용해 보았다.

1. 로그인 시 Refresh Token 저장

  • 로그인 성공 시, JwtProvider에서 Refresh Token을 발급
  • 발급받은 Refresh Token는 DB에 저장
  • HttpOnly 쿠키에 담아 클라이언트로 전달 
refreshTokenRepository.save(new RefreshToken(username, refreshToken, expiration));
response.addCookie(createCookie(refreshToken)); // Set-Cookie: Refresh=...

2. 재발급 요청 흐름

  • 클라이언트가 Access Token이 만료되었을 때, /api/v1/auth/reissue API로 쿠키에 있는 Refresh Token을 담아 요청
  • 서버는 다음을 두 가지를 검증
    1. Refresh Token이 유효한지 (validateToken)
    2. Refresh Token이 DB에 존재하며, 저장된 값과 일치하는지
if (!jwtProvider.validateToken(refreshToken)) throw new CustomException(INVALID_REFRESH_TOKEN);

RefreshToken saved = refreshTokenRepository.findById(username)
    .orElseThrow(() -> new CustomException(REFRESH_TOKEN_NOT_FOUND));

if (!saved.getRefreshToken().equals(refreshToken)) throw new CustomException(INVALID_REFRESH_TOKEN);

3. Refresh Token Rotation

  • 재발급 성공 시 기존 Refresh Token은 폐기하고, 새 토큰을 발급하여 쿠키에 담아 응답
  • DB에서도 저장된 Refresh Token 값을 갱신
String newRefreshToken = jwtProvider.generateRefreshToken(username, role);
saved.update(newRefreshToken, expiration);
response.addCookie(createCookie(newRefreshToken));

 

보안 강화 요소

항목 적용 내용
Refresh Token 탈취 방지 HttpOnly 쿠키 설정으로 JS 접근 차단
Refresh Token 유효성 검사 토큰 구조 파싱 + DB 조회 + 토큰 일치 여부 확인
Refresh Token 회전 새 토큰 발급 후 기존 토큰 무효화 (재사용 방지)
로그아웃 처리 Access Token을 블랙리스트에 등록하여, Access Token 만료시간 기준으로 차단
예외 처리 CustomException, GlobalExceptionHandler, ErrorCode 기반 통합 처리

 


💬 마무리하며

이번 JWT 토큰 재발급 구현을 통해 사용자 인증 흐름의 전체 생애주기를 학습하고 적용해볼 수 있었다.

Refresh Token을 이용한 인증 연장, Rotation 방식 도입, 쿠키 기반 보안 처리, 그리고 예외 흐름 통제까지 구현하면서 단순한 로그인/로그아웃을 넘어서 서비스 보안을 설계하고 제어할 수 있는 경험을 해볼 수 있어 좋았다.

 

현재는 Refresh Token과 블랙리스트를 모두 MySQL(DB)에 저장하고 있다. 추후 리팩토링을 통해 Redis와 같은 인메모리 저장소로 변경한다면 다음과 같은 장점을 얻을 수 있을 것이다.

  • 속도: DB보다 훨씬 빠른 읽기/쓰기 처리 속도
  • 만료 관리: TTL 설정으로 자동 만료 가능
  • 블랙리스트: Access Token을 블랙리스트로 등록할 때 TTL과 함께 저장하면 별도 스케줄러 필요 없음
  • 확장성: 멀티 서버 환경에서도 공유 캐시로 중앙 관리 가능