Skip to content

JWT 토큰 재발급 효율적으로 처리하기 V2

Sieun Ju edited this page Dec 3, 2023 · 4 revisions

요약 (Summary)

  • 네트워크 통신하다가 특정 타임때 쓰레드 잠깐 중단하고 동작 처리이후 재개 하는걸 구현합니다.
  • AKA, JWT 재발급 할때 가장 효율적으로 처리하도록 구현합니다.
  • 단순 Authenticator 에서 401 (토큰 만료) 일때 API를 재발급 하는게 아닌 효율적으로 처리하도록 합니다.
    • Authenticator 에서 처리하게 되면 다중 쓰레드 환경에서 재발급 API를 여러번 호출하는 이슈가 있음
  • 작업한 UI 클래스는 NetworkV2Fragment 입니다.

계획 (Plan)

  • OkHttp Builder 에서 지원하는 함수들과 OkHttp Docs 참고하여 OkHttp 라이브러리 구조에 대해서 다이어그램을 완성합니다.
  • 해당 기능에 필요한 API 를 개발합니다.
    • JWT 발급 받는 API
    • 서버에서 특정 시간 이후 API Response 주는 API 몇개 구성
  • 기존 성능에 큰 지장이 없도록 코드 구성이후 성능 테스트를 진행합니다.

설계 (Architecture)

OkHttp 구조 로그
이미지 이미지
  • Interceptor와 Authenticator 은 멀티 쓰레드 환경에서 이미 Runnable "run" 하기 때문에 해당 클래스에서 처리하면 효율적으로 처리할수가 없습니다.
  • 디테일한 Log를 찍어보면서 OkHttp 구조에 대해서 다이어 그램 완성 (상위 표 참고)
  • Dispatcher 라는 곳에서 ThreadPoolExecutor 를 매개변수로 받는데 여기서 쓰레드를 제어하여 요구사항에 맞게 API 제어가 가능하도록 설계

구현 (Implements)

  • Custom ThreadPoolExecutor 를 생성 쓰레드 작업을 진행하기전 호출하는 함수인 beforeExecute 에서 제어 하도록 구현
  • Java ThreadPoolExecutor 에서 제공하는 API 들은 오래간만에 자바 Docs 가서 참고
  • 오라클 도큐먼트 예제
class PausableThreadPoolExecutor extends ThreadPoolExecutor {
   private boolean isPaused;
   private ReentrantLock pauseLock = new ReentrantLock();
   private Condition unpaused = pauseLock.newCondition();

   public PausableThreadPoolExecutor(...) { super(...); }

   protected void beforeExecute(Thread t, Runnable r) {
     super.beforeExecute(t, r);
     pauseLock.lock();
     try {
       while (isPaused) unpaused.await();
     } catch (InterruptedException ie) {
       t.interrupt();
     } finally {
       pauseLock.unlock();
     }
   }

   public void pause() {
     pauseLock.lock();
     try {
       isPaused = true;
     } finally {
       pauseLock.unlock();
     }
   }

   public void resume() {
     pauseLock.lock();
     try {
       isPaused = false;
       unpaused.signalAll();
     } finally {
       pauseLock.unlock();
     }
   }
 }
  • beforeExecute 에서 특정 시간, 특정 API 호출하고 정상적으로 받으면 다시 쓰레드 재개하도록 구현 (JWT Refresh API)
  • 클라이언트 입장에서 네트워크 환경은 제각각이라 특정 API 호출하는게 실패하면 다시 요청할수 있는 재시도 정책 을 추가 구현

이슈사항 (Issue)

  • 기존 RxErrorHandlingCallAdapter 설정값 이슈
    • 네트워크 통신시 CacheThread 로 자동 처리되도록 하는 설정 RxJava3CallAdapterFactory.createWithScheduler(Schedulers.io()) 을 했더니 ThreadPoolExecutor 에서 beforeExecute 호출이 안됨 단, 직접 구현한 코루틴은 해당 함수 호출하는 것으로 확인
    • 해당 힌트를 얻어 내부 RxJava3CallAdapterFactory 코드 분석
RxJava3CallAdapter.java
    @Override
    public Object adapt(Call<R> call) {
    Observable<Response<R>> responseObservable =
        isAsync ? new CallEnqueueObservable<>(call) : new CallExecuteObservable<>(call);
        // do something..
    return RxJavaPlugins.onAssembly(observable);
    }
  • 상위 코드에서 isAsync 에 따라서 CallEnqueueObservable or CallExecuteObservable 나뉨
  • RxJava3CallAdapterFactory.createWithScheduler 로 처리하는 경우 isAsync = false,
    RxJava3CallAdapterFactory.create 로 처리하는 경우 isAsync = true 로 구성이 되고,
    CallEnqueueObservable 와 CallExecuteObservable 는 OkHttp enque 와 execute 를 Rx 스타일로 처리하는 Wrapper 클래스임을 확인
  • RxErrorHandlingCallAdapter 에 create() 함수로 변경하고 작업하는 구간에 subscribeOn 을 넣어서 네트워크 통신시 CacheThread 를 처리하도록 대응

테스트 (Test)

  • 요구조건에 맞게 기존 성능과 비슷해야함 (큰 저하는 이루어져서는 안됨)
  • JWT 토큰이 만료 되기 N초 전에 API 통신하려던 쓰레드들 잠시 중단하고 JWT 토큰을 재발급 받은 이후 다시 재개하는지 확인
  • JWT 재발급 API 는 특정 시간에 한번만 호출되어야 하며 재발급 토큰으로 HTTP Header 에 다시 셋팅되어 처리되는지 확인
  • 쓰레드를 제어한다고 해서 Single Thread로 네트워크 통신하는 쓰레드를 제어해서는 안되며, API 를 병렬적으로 처리할때 제대로 구현되는지 확인
  • 10분동안 JWT 관련 API를 병렬로 호출 했을때 Authenticator 로그가 찍히는지 확인 (절대 해당 로그가 찍히면 안됨)
  • 토큰 Refresh API 는 만료되기 5초 전에 호출 (빡쎈 테스트를 위함)

테스트 환경

  • JWT Refresh API를 5초 이후에 받을수 있도록 API 스펙 구성
router.post("/refresh", (req, res) => {
  // Refresh Token..
  // var token = req.header("Authorization");
  var email = req.body.email;
  var timeDelay = req.body.delay ? req.body.delay : 0;
  var expiredTime = req.body.expiredTime ? req.body.expiredTime : "5m";
  // 만료 1분
  var refreshToken = jwt.sign(
    {
      type: "JWT",
      nickname: email,
    },
    jwtSecret,
    {
      expiresIn: expiredTime,
      issuer: "sieun ju",
      algorithm: "HS256",
    }
  );
  setTimeout(function () {
    res
      .status(200)
      .send({
        status: true,
        data: {
          payload: {
            token: refreshToken,
          },
        },
      })
      .end();
  }, timeDelay);
});
  • 로그를 제대로 볼수 있게 tag 는 "Network_Test" 로 셋팅
    • 주요 클래스 위치에 Log 처리 PauseAbleThreadPoolExecutor, TokenAuthenticator, HeaderInterceptor, RefreshTokenInterceptor 로그 추가
TokenAuthenticator.kt
Timber.tag("Network_Test").e("Authenticator ${req.url.encodedPath}")

PauseAbleThreadPoolExecutor.kt
Timber.tag("Network_Test").w("Dispatcher 토큰을 재발급합니다. ${t.name}")

HeaderInterceptor.kt
val str = StringBuilder()
str.appendLine("Code ${response.code} Request ${response.request.url.encodedPath} (${tookMs}ms)")
str.appendLine("Authorization: ${req.header("Authorization")}")
Timber.tag("Network_Test").d(str.toString())

RefreshTokenInterceptor.kt
val tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs)
val str = StringBuilder()
str.append("Code ${response.code} Request ${response.request.url.encodedPath} (${tookMs}ms)")
Timber.tag("Network_Test").w(str.toString())
  • JWT 토큰 만료시 HTTP Status Code 401 로 리턴하는 API 추가
router.get("/jwt/test", (req, res) => {
  var token = req.headers.authorization;
  let decoded = null;
  try {
    let timeDelay =
      req.query.timeDelay !== null && req.query.timeDelay !== undefined
        ? req.query.timeDelay
        : 0;
    decoded = jwt.verify(token, jwtSecret);
    setTimeout(function () {
      res
        .status(200)
        .send({
          status: true,
          data: {
            payload: {
              message: "JWT Token Test",
            },
          },
        })
        .end();
    }, timeDelay);
  } catch (err) {
    res
      .status(401)
      .send({
        status: false,
        message: err,
      })
      .end();
  }
});

테스트 결과 (Test Result)

로그 기록

Clone this wiki locally