Refresh Token 구현 트러블슈팅: Body vs Header 방식

oversleep·2025년 2월 24일
0

troubleshooting

목록 보기
7/19
post-thumbnail

발단: Access Token 만료 처리 구현

JWT 기반 인증에서 Access Token이 만료되었을 때 Refresh Token으로 새로운 토큰을 발급받는 과정을 구현하면서 여러 시행착오를 겪었습니다.

일반적인 구현 방식 (Request Body)

보통 Refresh Token은 Request Body에 담아 전송합니다:

const response = await axios.post(
  "/auth/refresh",
  refreshToken,
  {
    headers: {
      "Content-Type": "text/plain"
    }
  }
);

이 방식이 선호되는 이유:
1. 토큰은 중요한 인증 정보이므로 body에 담아 전송
2. Request body는 암호화되어 전송 가능
3. 서버 로그에 헤더보다 body가 덜 노출됨

실제 구현 과정에서 마주친 문제들

여러 방식을 시도했지만 계속 실패:

  1. Body에 직접 토큰 전송:
// 401 Unauthorized 에러 발생
  1. Bearer 포함해서 전송:
// "Compact JWT strings may not contain whitespace" 에러
  1. JSON 형태로 전송:
// "Cannot deserialize value of type 'java.lang.String'" 에러

해결: Header 방식 채택

백엔드 개발자와의 소통 결과, 헤더에 담아 전송하는 방식으로 결정:

const response = await axios.post("/auth/refresh", null, {
  headers: { refreshToken }
});

Header 방식 채택의 의미

일반적인 방식(Body)과 다른 방식(Header)을 선택한 이유:

  1. 서버 구현의 특성

    • 서버에서 헤더를 통한 토큰 처리가 더 용이
    • 기존 인증 로직과의 일관성
  2. API 설계의 RESTful 특성

    • 인증 관련 정보는 헤더를 통해 전달
    • Body는 실제 리소스 데이터를 위해 사용
  3. 구현의 단순화

    • 헤더 방식이 구현이 더 단순
    • Content-Type 고려 불필요

무한 루프 방지 로직

토큰 갱신 과정에서 중요한 것은 무한 루프 방지:

if (
  error.response?.status === 500 &&
  error.response?.data?.message?.includes("JWT expired") &&
  !originalRequest._retry
) {
  originalRequest._retry = true;
  // 토큰 갱신 로직
}

_retry 플래그로 재시도 여부를 체크하여 무한 루프를 방지합니다.

교훈

  1. API 설계는 정답이 없음

    • 일반적인 방식이 있지만, 상황에 따라 다른 접근이 필요
    • 백엔드와의 긴밀한 소통이 중요
  2. 문서화의 중요성

    • API 스펙 문서(Swagger)의 정확성
    • 실제 구현과 문서의 일치 필요
  3. 보안과 편의성의 균형

    • 토큰 전송 방식의 보안성 고려
    • 구현의 복잡성과 보안성 사이의 트레이드오프

결론

Refresh Token 구현은 "이렇게 해야 한다"는 절대적인 규칙보다는, 프로젝트의 요구사항과 팀의 합의에 따라 유연하게 결정해야 합니다.
중요한 것은 선택한 방식에 대한 명확한 이유와 이해, 그리고 팀원 간의 합의입니다.

profile
궁금한 것, 했던 것, 시행착오 그리고 기억하고 싶은 것들을 기록합니다.

0개의 댓글