개탕 IT FACTORY

JWT refresh token 중복 호출 이슈 본문

Front-end

JWT refresh token 중복 호출 이슈

rendar02 2024. 10. 12. 00:25
반응형

개요

회사내에서 기존 로그인 방식을 변경하여 JWT 토큰 로그인 방식으로 변경하게 되었다.
JWT 토큰 로그인 방식은 많이 알려진 방식이긴 하나 대부분 백엔드에서 많이 처리해줘서 이번에 처음 프론트에 관심사가 모두 전달되어서 로그인 시 토큰관리 및 재갱신 부분도 만들게 되었다.

구현도중 토큰이 만료되었을시 실패했던 API 갯수만큼 refresh 토큰 재발급 api가 중복으로 호출되는 이슈를 확인하여 해결했던 부분을 남길려고 한다.

문제의 시작

회사내 로그인 시스템을 기존 백엔드에서 세션을 모두 관리하던 방식에서 → JWT 토큰 방식으로 변경하게되었다
물론 회사의 모든 백엔드 시스템과 프론트를 모두 교체해야되는 대규모 공사여서 굉장히 부담이 되는 프로젝트였던 것은 사실이다
(사실 프론트팀과 같이 할려했으나 회사내 또 다른 대규모 프로젝트건 때문에 혼자 하게되었다)

 

세션 → JWT 방식은 사실 단순하게 관리주체가 어디냐에 따라 다르지만,
이번에 추후 Nextjs 프로젝트를 대비(현재는 완료된 프로젝트)하여 관리 주체를 프론트로 넘어오게되었다.

 

간단한 구조를 설명하자면 아래와 같다

로그인 → JWT토큰 Store or Cookie에 저장 → API 호출 → header내에 Bearer ${token} 형식으로 Auth 전달 → API 응답 → 처리 → 토큰 만료시 → refresh token → JWT 재발급 → store / cookie 저장 → api 호출

 

🚨 토큰 만료시간은 보통 2시간으로 설정하지만 이부분은 회사내규나 회사 보안시스템에 따라 다른점 참고

 

문제의 시작은 바로 API 호출이 아닌 토큰 만료시 재호출시 발생하였다.

처음 사용했던 방식의 경우 axios interceptor 를 활용해서 중간에 요청을 가로채서 재발급 시킬려고 하였으나 문제는 재발급요청이 내가 실패했던 요청만큼 갔던 것이다

참고이미지 (링크:https://klloo.github.io/memoization-api/)

당시 사진을 못찍어 없지만 위와 같은 형태로 요청이 왔었습니다.

당시 코드를 첨부할수 없지만,
검색해서 돌아다니는 코드를 통해서 inerceptor 코드를 해결하였지만 위와같은 문제가 발생하였다

몇가지 방식을 통해서 해결은 하였지만 좀 이상한 방식으로 해결한 부분이 있어서 고치고 고쳐서 해결한 부분을 공유하도록하겠다.

1. Tanstack-Query 사용

가장 손쉽게 사용할수 있는 부분이였지만,

Nextjs 변환전 회사 front코드의경우 react + redux로 API 호출을 담당하고 캐싱까지 해두었다…
쉽게말하면 모든 구조를 변경해야되므로 이부분은 해결 범위에서 벗어난 부분이라 딱히 해결법이 되진 못하였다.

2. Lock 코드를 통해 API 중복호출 방지

lock을 전역코드로 사용하여 lock시에는 기존 api코드를 발생시키고 그렇지 않을경우 refresh 코드를 실행하여 갱신하였다

  let lock = false;
  baseApi.interceptors.response.use(
    async (response) => {
            // 내부 코드 생략
      if (lock) {
          // 이쪽에서 코드 실행
      }
      lock = true;
      // refresh 코드 발생 
      const accessToken = await refreshTokenAPI(baseApi, cookies.get('refresh_token'));
      // 내부 코드 생략
      return response;
    }

잘 작동되었지만 사실상… 일단 댐 구멍에 땜질해서 막은 꼴밖에 안보였다 그래서 더 서치하던중 가장 좋은 방법을 찾게 되었다

3. Observer 패턴을 사용 (채택)

https://ko.wikipedia.org/wiki/옵서버_패턴

Observer 패턴이란

옵저버(관찰자)들이 관찰하고 있는 대상자의 상태가 변화가 있을 때마다 대상자는 직접 목록의 각 관찰자들에게 통지하고, 관찰자들은 알림을 받아 조치를 취하는 행동 패턴이다.

이 패턴을 적용하면 현재 interceptor에서 받은 정보를 계속 관찰하면서 refresh 상태에 따라 분리가 가능하겠다고 판단하였다.

사실 클래스를 오랜만에 짜보기도 하고 Observer패턴은 직접 짜본적은 없었다.
이번에 gpt의 도움과 다른 블로그를 참고하여서 개발하면서 패턴을 직접 만들어보았다.

class Observable {
  private observers: Array<(data: any) => void> = [];

  subscribe(observer: (data: any) => void): () => void {
    this.observers.push(observer);
    return () => (this.observers = this.observers.filter((obs) => obs !== observer));
  }

  notify(data: any): void {
    this.observers.forEach((observer) => observer(data));
  }

  clearObservers(): void {
    this.observers = [];
  }
}

사용은 아래처럼 사용하면된다

const tokenObservable = new Observable();

class TokenManager {
  private isRefreshing = false;

  constructor(private axiosInstance: AxiosInstance, private cookies: Cookies) {}

  async refreshToken() {
    if (!this.isRefreshing) {
      this.isRefreshing = true;
      try {
                // 리프레시가 발생했을때 코드 
        return response;
      } catch (error) {
        // 에러가 발생했을시 코드 
      } finally {
        this.isRefreshing = false;
      }
    }

// 만약 토큰이 갱신 중이라면, 새로운 토큰이 갱신될 때까지 대기합니다.
    return new Promise<void>((resolve) => {
    // 토큰 갱신이 완료되면 구독된 함수가 호출되고, 이때 resolve가 호출됩니다.
      tokenObservable.subscribe((token) => {
        resolve();
      });
    });
  }
}

export default TokenManager;

가장 확실하고 우아하게 짠 코드가 아닐지 모르겠다.

사실 tanstack-query를 사용했다면 더 쉽게 캐싱과 refetch를 날릴수있었겠지만
마땅한 대안이 없었던 레거시에서 사용하다보니 만들었던 코드고 찾았던 코드였던 것 같다.

마무리

오랜만에 클래스 관련된 코드를 짜고 재미있던 것 같다.
매번 함수형 코딩, 프로그래밍만 짜오다가 클래스를 보니 오히려 머리를 쓰게 되고, 또 학창시절배웠던 지식을 복기한 느낌이 들었다.

사실 인증같은 경우 사용자의 최상단에서 사용하는 부분이라 가장 중요하고 보안적으로 필요한 부분인데
어떻게 관리해야될지 아니면 어떻게 코드를 짜야될지 고민이 많았던 부분이였던 것 같다.

짜면서 access-token과 refresh-token 관리 방향성 그리고 이것을 저장하는 방법 탈취되었을시 가장 안전한 방법은? 하면서 생각을 많이하였다
결론은 시스템적으로 이상없었고, 사용성도 해치지 않았다 (현재는 Nextjs로 넘어가면서 사실상 레거시코드)

참고자료

https://velog.io/@chanwoo00106/토큰-재발급-중복-요청-문제

https://klloo.github.io/memoization-api/

https://inpa.tistory.com/entry/GOF-💠-옵저버Observer-패턴-제대로-배워보자

반응형