저는 항상 앱을 개발하면서 API요청을 보낼 때 항상 "요청 횟수 제한"을 걸어두는 편이에요.
별건 아니지만, 과거 처음으로 진행한 프로젝트에서 실수로 무한으로 API요청을 하는 코드를 작성하여 문제가 생긴 뒤로 항상 신경써서 챙기는 부분이랍니다.
이후로도 다양한 프로젝트를 진행하면서 그 방식도 다양하게 시도해보게 되었는데요, 오늘은 그 과정에서 시도한 다양한 방식을 이야기해볼게요!
가장 처음으로 적용했었던 요청 제한 방식으로, 단순하게 버튼을 한번 누르면 특정 시간 동안 요청을 무시하는 방식이에요.
var btnClickTime by remember { muatableStateOf(0L) } //... Button{ onClcik = { val times = System.currentTimeMillis() if(btnClcikTime + 500 < times){ btnClickTime = times viewModel.call() } }
후술할 Throttling
의 View
버전이죠!
버튼이 여러개더라도 한번에 적용할 수 있고, 매우 간단하지만, 요청을 제한하는 로직이 View
영역에 있어 코드가 복잡해지고 중복이 많아졌어요.
그리고 해당 기능은 서로 다른 파일, 컴포저블에서 요청하는 경우에 적용할 수 없는 문제가 있었어요.
위의 문제들은 View
에서 요청제한을 적용하려해서 생긴 문제들이에요. 그래서 이번에는 요청을 받아들이는 뷰모델
에서 직접 제한을 적용하기로 하였어요.
var lastTime = 0L fun getA() { val tiems = System.currentTimeMillis() //원래는 외부에서 주입받음 if(lastTimes + 1000 > times) return lastTimes = times viewModelScope.launch{ //... } }
이제 앞서 이야기한 중복코드나 서로 다른 컴포저블에서 시간을 공유하지 못하는 문제를 해결했어요.
하지만 쓰로틀링
의 경우 결국 1초 마다 한번씩은 요청이 가요. 만약 무한 요청이 된다면 간격만 넓어졌지 무한 요청인 것은 동일한 문제가 있었어요.
그래서 최종으로 온 요청 한가지만 받도록 코드를 수정하였어요.
기본적으로 viewModelScope
는 코루틴 스코프를 열면서 Job
클래스의 객체를 반환해요. 이 Job
은 코루틴 스코프의 실행 상태를 담고있기에 Job
을 통해 코루틴이 실행중인지, 어떤 상태인지와 함께 취소를 시킬수도 있어요.
즉, Job
을 저장하여 상태를 확인하고 실행중이라면 취소시키고 새로운 스코프를 여는거죠!
var job: Job? = null fun getA() { if(job!=null&&job.isCompleted.not()) job!!.cancel() job = viewModelScope.launch{ //... } }
이 방식은 상당히 괜찮다 생각해서 오래 사용했어요.
다만 문제는 잘 진행중이던 Job
을 취소시킨다는 부분이에요. 즉 이미 요청도 갔고, 응답이 거의다 왔는데 취소되어서 다시 시작하게 되는 일이 생길 수 있었어요.
요청 자체를 막을 수 없는거죠!
또 AI를 쓰는 일이 많아지며 오래걸리는 작업이 늘어나 문제가 될 가능성이 있었어요.
그래서 이번에는 기존에 작업을 취소하는 것이 아닌 특정 시간동안 들어온 요청에서 가장 마지막 것만 받아들여 수행하는 Debounce
를 적용하기로 하였어요.
Debounce
의 경우 flow
에서 기본으로 제공해주기에 적용하기도 다른 방식에 비해 간단하답니다.
viewModelScope.launch{ dataSource.getA()//flow .debounce(1000) .collect { value -> } }
Flow
와 Debounce
를 결합하는게 가장 쉽기도 하고, 깔끔하게 요청횟수를 제한하는 방법이라 판단하고 이후에는 계속 Debounce
를 사용하고 있어요.
이제 요청 횟수 제한에 더 이상의 변경은 없을 줄 알았지요.
하지만 이후 점점 복잡한 요청과 여러 API 요청을 종합하여 뷰모델
로 반환해야하는 일이 생겨났어요.
하나의 API를 무한 요청하는 것은 디바운스
로 막을 수 있지만, A API 결과로 다시 A API가 호출되거나, A -> B -> A로 반복되는 것은 막을 수 없었어요.
그래서 이번에는 아예 API 요청에 횟수 제한을 걸기로 결정합니다.
Android에서 API 호출시 자주쓰는 Retrofit
은 Okhttp
를 기반으로 만들어져있어요.
그리고 이 OKhttp
에는 API 호출시 이를 가로채 무언가 처리하거나, 붙이거나 할 수 있게 해주는 Interceptor
를 추가할 수 있어요.
이 인터셉터를 통해 요청이 올때마다 카운트하고 초당 몇회의 요청이 왔는지 관리하는 거죠!
class RateLimitingInterceptor : Interceptor { private val requestTimestamps = mutableListOf<Long>() override fun intercept(chain: Interceptor.Chain): Response { val now = System.currentTimeMillis() synchronized(this) { requestTimestamps.removeAll { now - it > 1000 } if (requestTimestamps.size >= 5) { throw LimitExceedException("요청이 너무 자주 발생했습니다.") } requestTimestamps.add(now) } return chain.proceed(chain.request()) } }
이렇게 하면 1초이내에 5회이상 연산이 오게되면 LimitExceedException
을 던집니다. 이후에 try catch
를 통해 이를 잡아 탈출하면 되는거죠!
인터셉터까지 가니 조금 과하게 요청 횟수에 제한을 두려하는 것 같기도하네요...?!
하지만 디바운스까지는 모든 프론트 사람들이 적용했으면 좋겠습니다.
저는 가장 앞에서 이야기한 무한 요청때 30분 정도 동안에 150만회의 요청이 갔었습니다. Firebase 무료 요금제였기에 돈은 나가진 않았지만, 조심해서 나쁠건 없지요!