ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 안드로이드 만료 토큰 갱신 / 요청 api에 토큰 삽입 자동화 시스템 개발기 - 1 전반적인 설계 편 [Android Retrofit Auto Insert Token & Auto Refresh Token]
    안드로이드 2021. 8. 31. 18:40
    반응형

    자동화....? 가 아무튼 맞다.

    프로젝트를 개발해오면서 반드시 작성해야하는 로직이지만 반복되어서 중복 코드가 쌓여가고, 일일이 직접 코드를 작성해야하는 경험을 적지 않게 해왔다. 처음 개발을 시작할 때는 이런 단순 노동의 문제점이 무엇인지 알지 못했지만, 점점 개발을 하는 기간이 길어질 수록 이런 종류의 일을 자동화 하는 것에 관심을 많이 가지게 되었다.

    필수적으로 해야 하지만 똑같은 작업을 반복적으로 해야하는 일은 자동화 해두면 더 이상 그 작업을 하지 않아도 되어서 편리하고, 다른 작업들을 손쉽게 효율적으로 처리 할 수 있게 되기 때문이다.

    안드로이드에서 이런 작업들이 무엇이 있을까?

    제목이 곧 내용이라지만 좀 주저리를 써보면.... 안드로이드를 개발하다보면 생각보다 많은 단순 반복 작업들이 존재한다. Activity에 View를 binding 해주는 작업, 아키텍처 레이어 대로 인터페이스와 구현체들을 만들어 주는 것, 서버 응답을 가공하는 작업 등등... 무수히 많은 단순 반복 노동들이 존재한다.

     


     

    그래서 필자가 한 자동화는?!

    1. 토큰을 발급받으면 로컬 디비에 저장하고 재요청 시 만료되었다면 재발급 후 저장 및 반환
      • 이미 발급 받은 토큰이 만료되지도 않았는데 계속해서 새 토큰을 요청하는 것은 비효율적이라 판단해서 로컬 디비 저장 기능을 설계했다.
      • 여기에 추가해서, 디비 접근은 서버 통신 보다야 빠르겠지만 결국 IO를 발생시키기에 느리다. 그래서 해당 객체의 필드를 둠으로써 캐시 처리도 진행했다.
    2. api 요청 시 만료된 토큰 응답이 오면, 해당 요청에 재발급한 토큰을 재 삽입한 후 재요청 자동화
      • 이렇게 하면서 DataSource Layer에서는 토큰이 만료됐었다는 사실을 알지 못한 채 데이터를 수신하게 된다.
      • 모든 ViewModel 등에서 토큰이 만료되었을 때의 로직을 중복적으로 처리해야 하는데, 이 로직을 더 이상 ViewModel 등에 위치 시킬 필요가 없다!
    3. Retrofit Service Interface의 메서드 파라미터의 token 자리를 제거
      • 아래 사진을 보면 모든 메서드에 Token 관련 파라미터가 없다! (정말 어썸하다 😊😊)
      • 필자는 토큰 뿐만 아니라 더 많은 수의 필수 값들을 더 집어넣었어야 해서 효율이 더욱 좋았다.


     

    사내 인증 시스템 흐름은?

    우선 토큰을 발급 받기 이전에, 사내의 인증 시스템이 어떻게 돌아가고 있는지 파악이 먼저였다.

    사내 api를 호출하기 위해 토큰을 발급 받는 과정은 크게 두 절차가 필요했다.

    1. 사내 SDK를 통해 이메일/패스워드를 인풋으로 넣고 Mobile 인증 Token을 발급 받기.
    2. Mobile 인증 Token을 넣고 사내 인증 시스템 api를 호출해서 Online Token 발급 받기.

    이 두 절차를 통해 발급받은 Online Token을 넣으면 사내 api를 모두 호출 할 수 있게 된다.

     


     

    전체적인 자동화 시스템 구조

    로그인 후, 인증 API가 아닌 사내 Service API를 처음 호출하는 과정의 전체 흐름도를 그려보았다.

    사용자가 앱을 맨 처음 설치 후에 서비스 API 를 호출하게 되는 경우 아래와 같은 순서대로 로직을 수행한다.

    1. SDK로부터 모바일 인증 토큰을 가져온다.
    2. Online Token을 발급 받는 요청을 Interceptor에서 가로챈 뒤, 모바일 인증 토큰을 삽입해서 요청을 보낸다.
    3. 위 응답으로 Online 토큰을 가져온다. (디비에 저장도 한다)
    4. 서비스 API 호출한 요청을 Other Interceptor에서 가로챈 뒤, Online Token을 삽입해서 요청을 보낸다.
    5. 요청 성공!

    위 과정에서 처음에 만들고자 했던 모든 자동화 프로세스가 동작한다.

    그리고 얻는 이점은 아래와 같다.

    1. API를 호출할 때, Token에 관련된 작업을 개발자가 직접 다뤄야할 일이 매우 적어진다.
    2. 토큰 만료시 재발급 후 재요청 하는 로직을 따로 작성할 일이 거의 없다.

     


     

    에러 핸들링

    모든 서버 API 요청은 Repository / RemoteSource Layer로 나뉘어져 있기 때문에 Retrofit 에서 반환하는 응답 값을 그대로 사용하기가 매우 까다로웠다.

    그래서 ViewModel에서 최종 사용하는 형태를 상상하면서 아래처럼 손으로 끄적이며 설계를 해봤다.

    첫 번째가 Coroutine의 특성을 그대로 살리는 형태이다.

    • 비동기를 마치 동기처럼 작성함으로써 매우 직관적인 코드를 작성할 수 있었다.
    • 서버 실패 응답을 Exception을 Catch 하는 것 이외엔 방법이 없었다. (Result<T>를 사용했으면 나았을까?)
    • Coroutine Exception Handler의 개수가 많아질 가능성이 있다.

     

    두 번째는 응답을 Callback 형태의 다수의 람다로 받는 구조이다.

    • 이렇게 설계할 바에야, 차라리 Rx를 사용하는 것이 더 직관적이지 않을까?
    • 한 번 호출하는 데에 코드가 매우 길다. Callback Hell 발생 가능. (Coroutine 왜 씀?)
    • 원하는 종류의 Response 객체를 커스텀해서 받을 수 있다.
    • 각 API 호출 별로 에러 처리가 가능하다.

     

    두 방법 모두 각자의 장단점이 명확하다고 생각했다. 같이 프로젝트 진행하는 동기분이랑 상의도 좀 해보고, 매우 많은 고민을 한 끝에, 결국 첫 번째 방법을 가지고 에러 핸들링을 하기로 결정했다.

     


     

    설계 시행 착오

    처음 부터 깔쌈하고 완벽한 설계를 할 수 있었다면 참 좋았겠지만.. 최종 설계까지 4번의 재설계 과정이 있었다 🤣🤣🤣

    그나마 설계 과정이 있어서 이렇게 시간이 적게 걸렸지, 처음 부터 코드부터 작성하고 4번 코드를 갈아 엎으면서 진행했더라면 더욱 느리고 좌절감이 들었을 것이다.

    여기에 그 과정을 다 쓰기에는 너무 길어지니 주요 이슈만 요약하자면,

    1. 사내 서버 응답의 Http Status Code는 전부 다 200으로 온다! 에러인 경우에도 200으로 온다!
    2. 위 경우 때문에 Retrofit의 Authenticator 기능을 사용할 수 없다! (토큰 갱신 때 쓰려했던 기능)
    3. api 요청시 필요한 토큰이 header에 들어가는 것이 아니라, 요청 유형에 따라 각각 삽입 위치가 다르다!

    위 녀석들 (+기타 짜잘한 녀석들) 덕분에 설계 버전이 1.0~1.4 버전까지 총 5개의 설계가 나왔었다 🤔

     

    서버 응답 핸들링

    모든 서버 응답에는 자체 에러코드와 메시지 필드가 존재했다. 정상 / 비정상 요청에 구애받지 않고 해당 필드는 항상 그대로 존재했기 때문에 이 자체 응답 코드를 가지고 서버 응답 성공 / 실패를 판단하는 로직을 작성했다.

    덕분에 예전에 겪고 잊어버린 아래 에러를 오랜만에 마주했다.

    java.lang.IllegalStateException: closed ....... 로그가 이게 다 라니

    어디에서 터진 것인지 조차 알 수 없지만.. 밑의 로그를 잘 보니 Gson 파싱에러라는 것을 알 수 있었다.

    이게 왜 발생할까.... 그리고 close?

     

    과거에 Retrofit을 사용하면서 엄청난 삽질을 했던 경험이 갑자기 머릿 속에서 스쳐지나갔다.

    Callback에서 response?.errorBody?.string()을 꺼낸 뒤, 동일한 값을 다시 꺼냈을 때 Exception이 발생해서 엄청난 삽질을 했던 경험이었다. 그땐 정말 아무것도 몰라 수 많은 시간을 날리고서야 결국 해당 값은 일회용으로만 쓸 수 있다는 것을 알아냈었다.

     

    이번 현상도 비슷했다. 과거의 아픈 추억을 떠올리면서 문제의 부분을 찾아냈다. 모든 서버 응답이 성공으로 떨어지고, 자체 에러코드를 넘기는 것 때문이었다!

    자체 코드를 식별하기 위해서 response.body.string() 을 파싱하기 위해 접근했고, 해당 객체 또한 일회용이기 때문에 발생한 문제였다. 이미 한 번 꺼냈기 때문에 내용이 비어서, 다음 Gson이 파싱할 내용이 없기 때문에 파싱 에러가 발생하는 것이었다.

     

    이 문제를 해결하기 위해서 OkHttp의 HttpLoggingInterceptor 객체를 뜯어보면 어떨까? 하고 생각했다.

    왜냐면, 이 친구는 모든 통신에 대해 로그를 찍어주는데, 그렇다는 것은 body를 접근한다는 의미이기 때문이다. 그래서 해당 코드를 좀 뜯어봤는데... 파악하기가 너무 어렵고 생각보다 코드량이 길어서 다른 방법을 찾게되었다.

     

    다음 찾은 방법은 Response.body를 접근하는 것이 아닌, Response.peekBody를 접근 하는 것이다. LogginInterceptor처럼 쓰는 것 보다는 이 메서드를 활용하는 편이 훨씬 낫다고 생각해서 이 메서드를 활용해 사용했다. 

    val jsonString = response.peekBody(Long.MAX_VALUE).string()

    peekBody의 파라미터로는 가져올 string의 byteCount를 넣으면 된다. 존재하는 string의 count 보다 큰 값을 넣으면 모든 string을 반환해준다. 그래서 그냥 Long의 최댓값을 집어넣었다. 그러면 아무리 길어도 다 뱉어주겠지.

     


     

    마무리

    토큰 재발급과 삽입 하는 자동화 시스템의 전체적인 설계 과정에 대해서 다루어보았다. 이전 부터 이런 식으로 자동화 시스템을 한 번 만들어 보고 싶었는데, 직접 설계부터 구현까지 다 해볼 수 있었던 값진 경험이었다.

     

    다음은 구체적으로 Repository 와 DataSource Layer를 어떻게 구성했는 지 써 보려고 한다. 그 다음으로는 Token을 어떻게 자동으로 Request에 집어넣었는지도 적을 생각이다.

     

     

    이 글이 도움이 되셨나요?

    말리빈에게 커피를 한 잔 쥐어 주세요! ☕ 커피를 인풋으로 더 좋은 글을 아웃풋으로 쓰려 노력하겠습니다 😊

     

    Buy Me A Coffee

     

    반응형

    댓글

Designed by Tistory.