본문 바로가기
프로그래밍 개발/IT 서비스 개발 운영

실제 서비스 운영중 생긴 API 인증(Certified) 이슈 트러블슈팅과 Axios 인터셉터(interceptors)

by Jinseok Kim 2023. 8. 21.
반응형

실제 서비스 운영 중 생긴 API 인증(Certified) 이슈 트러블슈팅과 Axios 인터셉터(interceptors)

운영 중인 서비스 프로젝트에는 Axios라는 API 연동 라이브러리를 사용하고 있습니다. Axios 기능을 통해 RestFul 한 기능들을 만들고 기능을 생산하는데 많은 도움을 주고 있는 라이브러리입니다.

 

Axios interceptors는 API을 관리하는데 아주 유용하면서도 필수적이라고 생각합니다. request/response 둘 다 공통적으로 호출 작업을 실행하기 전 처리하는데 매우 유용합니다.

 

Axios interceptors의 간단한 사용 방법과 Axios interceptors을 활용을 통한 서비스 이슈 해결을 기록하려 합니다.

 

 

 

Axios interceptors 란?


 

인터셉터 | Axios Docs

인터셉터 then 또는 catch로 처리되기 전에 요청과 응답을 가로챌수 있습니다. axios.interceptors.request.use(function (config) { return config; }, function (error) { return Promise.reject(error); }); axios.interceptors.response.use(f

axios-http.com

1) 정의

interceptors는 요청하기 직전 혹은 응답을 받고 then, catch(tryCatch도 가능합니다.)로 처리 직전에 가로챌 수 있습니다.

 

 

2) 구성

크게 3가지로 구성됩니다.

 

1. 인스턴스 생성

2. request(요청) 설정

3. response(응답) 설정

 

 

3) 예시 코드

import axios from 'axios'

// axios 인스턴스를 생성합니다.
const instance = axios.create({
    baseURL: 'https://api.hnpwa.com',
    timeout: 1000
  });

/*
    1. 요청 인터셉터
    2개의 콜백 함수를 받습니다.
*/
instance.interceptors.request.use(
    function (config) {
        // 요청 성공 직전 호출됩니다.
        // axios 설정값을 넣습니다. (사용자 정의 설정도 추가 가능)
        return config;
    }, 
    function (error) {
        // 요청 에러 직전 호출됩니다.
        return Promise.reject(error);
    }
);

/*
    2. 응답 인터셉터
    2개의 콜백 함수를 받습니다.
*/
instance.interceptors.response.use(
    function (response) {
    /*
        http status가 200인 경우
        응답 성공 직전 호출됩니다. 
        .then() 으로 이어집니다.
    */
        return response;
    },

    function (error) {
    /*
        http status가 200이 아닌 경우
        응답 에러 직전 호출됩니다.
        .catch() 으로 이어집니다.    
    */
        return Promise.reject(error);
    }
);

 

 

 

 

 

⚠️ 서비스 운영 이슈 발생과 해결 과정


어느 날 서비스 운영 중 곧바로 알아차릴 수 없는 치명적인 이슈가 발생하는 것을 알게 되었습니다. 처음에는 프런트단의 오타 혹은 백 단의 문제인 줄 알고 꽤 긴 시간 동안 헤매었습니다.

 

증상은 Access Token가 필요한 Request API header에 담아 호출하여 백 단에서 Access Token을 받아내어 처리하여 Request에서 받아온 인증 처리가 된 데이터가 의도치 않은 값을 업데이트하여 받아오는 이슈였습니다.

 

request.headers.common.authorization = `Bearer ${AccessToken}`;

 

예를 들어 Access Token을 API header을 통해 받아내어 백 단의 Token 인증을 통해 해당 게시물에 유저가 좋아요/싫어요를 했는지 판별하는 데이터가 있었습니다.

 

이때 게시물 데이터를 Refresh 하는 기능이 있었는데 이 기능을 작동하여 다시 API을 호출하면 좋아요/싫어요를 판별하는 데이터가 의도치 않게 바뀌어 들어오는 이슈였습니다.

 

 또 어려웠던 점은 이슈의 원인을 재현하기 어려웠습니다. 짧은 타이밍으로 Refresh을 하면 증상이 재현되지 않았지만 5분이 넘어가면 증상이 재현되는 산발적인 이슈였습니다.

 

단서는 Access Token을 header을 통해 받아내어 백 단에서 처리하는 작업, 그리고 5분이 넘어가야지 증상이 발현된가는 것이었습니다.

 

 

 

곰곰이 생각해 보다가 결국 발견한 최종적인 원인은 설정해 둔 Access Token의 만료 시간이5분이었고 5분이 지난 상태로 Refresh 기능을 작동시켜 API을 다시 호출하는 타이밍일 때 Access Token이 만료된 토큰이었고 이때 백 단에서 이미 만료된 토큰을 받아내어 제대로 의도된 업데이트 처리를 하지 못하고 있었습니다.

 

즉 response interceptors에서는 토큰 갱신 API을 쏴주고 있었는데 request interceptors에서는 토큰 갱신 API을 쏴주는 처리를 해주고 있지 않고 있었습니다.

 

원인을 발견한 저는 request interceptors에 바로 조치를 취하여 이슈를 해결하고 긴급 배포를 하게 되었습니다.

 

 

아래와 같이 interceptors 로직을 수정해 주어 토큰이 만료된 시점(토큰 만료되기 10초 미만으로 기준을 잡았습니다)에서 API을 호출해도 토큰을 갱신하는 작업이 끝난 후에 본격적인 API 작업 호출하도록 조치를 취하였습니다.

  restApi.interceptors.request.use(
    async request => {
      try {
        const token = await getStorage(TOKEN_SAVE_NAME);
        const refresh_token = token ? JSON.parse(token).refresh_token : '';

        if (token) {
          // 서버에서 오는 expired_at = unix time ==> 실제 날짜 구하려면 1000을 곱해야함
          const expired_at = JSON.parse(token).expired_at;

          // 토큰 만료 남은 시간초
          const expirTimeGap =
            Number(expired_at) - Number(dayjs().utc().unix());

          // 토큰 만료 남은 시간 10초 미만 -> access_token 갱신 로직 발동
          if (expirTimeGap < 10) {
            const response = await axios
              .create({
                baseURL,
              })
              .get('{토큰 갱신 API}', {
                headers: {
                  refresh_token,
                },
              });

            if (response.status === 200) {
              request.headers.common.authorization = `Bearer ${response.data.access_token}`;
              await setToken(response.data, null);
            } else {
              await removeStorage(TOKEN_SAVE_NAME);
            }
          } else {
            const access_token = token
              ? JSON.parse(token).access_token.trim()
              : '';
            request.headers.common.authorization = `Bearer ${access_token}`;
          }
        }
        return request;
      } catch (error) {
        return Promise.reject(error);
      }
    },
    async error => {
      return Promise.reject(error);
    },
  );

 

 

 

 

마무리하며...


 

인증 관련된 이슈는 제가 지금까지 운영 중에 일어난 가장 중요하면서도 원인을 찾기 어려운 경험이었던 것 같았습니다. 이 글에서 기록한 이슈 말고도 인증 관련 이슈들은 꽤 많이 있었습니다.

 

이러한 다양하고 찾기 어려운 이슈 디버깅 및 해결을 통해 IT 서비스 운영에 대한 역량을 높이게 된 계기가 되었던 것 같습니다.

 

 

반응형

댓글