본인이 전담해서 운영 중인 어플리케이션은 로그인을 하거나 특정 API를 호출할 때, 서버에 로그를 남긴다.
앱 운영을 전담하기 이전부터 있던 고질적인 문제가 있었는데.. 사용자가 로그인을 시도 할 때, 1초도 안되는 시간동안 소수초 간격으로 Api가 연속 호출되는 문제였다.
다수의 이용자가 사용하는 서비스이고, 최근 사용자가 늘어가는 만큼, 이대로 방치하면 서버의 과부하가 유발되어 서비스 품질이 낮아질 수 있겠다는 생각이 들었다.
마침 최근에 Medium에서 Debounce와 Throttle에 관련된 칼럼을 읽었던 기억이 있어서 적용해 보았고, 이슈를 해결할 수 있었다.
비슷한 이슈를 겪는 사람들에게 기술적 인사이트를 제공할 수 있겠다는 생각에, 관련 내용을 기록해 둔다.
Debounce와 Throttle의 개념
Debounce와 throttle은 전자 공학에서 기계적 스위치나 유체 흐름을 제어하는 개념으로 출발했지만, 소프트웨어 개발에서는 주로 이벤트 핸들링과 관련된 개념으로 사용된다.
이 둘의 공통점은 이벤트 발생을 제어하여 과도한 호출을 방지하고 시스템 자원을 효율적으로 사용하는 데 목적이 있다는 점이다.
개발자는 이를 통해 성능을 최적화하고 불필요한 연산을 줄일 수 있다.
공통점
- Debounce와 throttle 모두 특정 이벤트가 너무 자주 발생하는 것을 제어한다.
- 예를 들어, 사용자가 입력 필드에 빠르게 타이핑하거나 무한 스크롤에서 스크롤 이벤트가 빠르게 발생하는 경우, 이벤트 콜백의 호출 빈도를 조절할 수 있다.
- 결론은, 둘 다 불필요하거나 중복된 작업을 방지하여 시스템과 서비스의 안정성을 높이는 것이 주된 목적이다.
차이점
Debounce
- 이벤트가 발생한 후 일정 시간이 지나기 전에 다시 이벤트가 발생하면 타이머를 리셋한다. 최종적으로 일정 시간 동안 이벤트가 발생하지 않았을 때 한 번만 실행된다.
- 즉, 이벤트를 그룹핑하여 일정 시간동안 이벤트가 추가로 갱신되지 않으면 서버에 전송한다.
- 주로 사용자의 마지막 입력이 끝난 후, 작업을 실행하고 싶을 때 사용한다.
- 예: 검색 입력 시 실시간 자동완성
Throttle
- 일정 시간 간격으로 이벤트를 실행한다.
- 즉, 지정된 시간 동안에는 한 번만 이벤트가 발생하도록 한다.
- 주로 일정 주기마다 작업을 실행하고 싶을 때 사용한다.
- 예: 무한 스크롤, 윈도우 리사이즈 이벤트
각각의 구현 방법
Debounce
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking<Unit> {
// 예제 이벤트 흐름
val flow = flow {
emit("Event 1")
delay(100) // 100ms 대기
emit("Event 2")
delay(300) // 300ms 대기
emit("Event 3")
delay(100) // 100ms 대기
emit("Event 4")
delay(500) // 500ms 대기
emit("Event 5")
}
// debounce 연산자를 사용하여 이벤트 제어
flow
.debounce(200) // 200ms 동안 대기 후 마지막 이벤트 내보내기
.collect { event ->
println("Collected: $event")
}
}
위 코드는 200ms 동안 새로운 이벤트가 발생하지 않으면 마지막으로 수집된 이벤트를 방출한다.
주로 검색과 같은 텍스트 입력에 사용되기 때문에, EditText를 확장해서 사용할 수 있다.
fun EditText.textChanges(): Flow<CharSequence?> = callbackFlow {
val textWatcher = object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
trySend(s)
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
}
addTextChangedListener(textWatcher)
awaitClose { removeTextChangedListener(textWatcher) }
}
(TextWatcher 내용은 이 글 참조)
fun observeTextChanges(editText: EditText) {
lifecycleScope.launch {
editText.textChanges()
.debounce(300) // 300ms 동안 대기 후 마지막 입력값 내보내기
.collect { text ->
// 텍스트 변경 이벤트 처리
println("Text changed: $text")
}
}
}
이렇게 하면, 300ms 동안 대기한 후, 마지막 입력 값을 내보낼 수 있게 된다.
이를 통해 사용자가 빠르게 입력하는 도중에는 이벤트가 발생하지 않고, 입력이 멈추면 마지막 입력 값이 전달된다.
Throttle
class ThrottleManager(private val boundary: Long) {
private val lastCallTimes = mutableMapOf<String, Long>()
fun canProceed(key: String): Boolean {
val currentTime = System.currentTimeMillis()
val lastCallTime = lastCallTimes[key] ?: 0L
return if (currentTime - lastCallTime >= boundary) {
lastCallTimes[key] = currentTime
true
} else {
false
}
}
}
본인은 이렇게 ThrottleManager 클래스를 생성해서 관리한다.
여러 곳에서 손쉽게 재사용을 할 수 있기 때문이다.
특히, ViewModel에서 API호출을 제어할 때 유용하다.
private val throttleManager = ThrottleManager(2000L) // 2000ms의 최소 호출 간격
fun login(username: String, password: String){
if (throttleManager.canProceed("login")) {
loginApi(username, password)
}
}
위와 같이 사용하면 여러 메서드에서 동시에 Throttle을 제어할 수 있다.
만약, 비동기 데이터 슽트림을 처리하기 위해 Throttle을 구현해야 한다면, Flow를 사용해서도 구현할 수 있다.
Flow에서 기본으로 제공되는 Debounce와 달리, Throttle은 다음처럼 직접 작성해 주어야한다.
fun <T> Flow<T>.throttleFirst(windowDuration: Long): Flow<T> = flow {
var lastEmissionTime = 0L
collect { value ->
val currentTime = System.currentTimeMillis()
if (currentTime - lastEmissionTime >= windowDuration) {
emit(value)
lastEmissionTime = currentTime
}
}
}
Ref
'Android > 개인 기록' 카테고리의 다른 글
[Android Tip] Spinner: DropDown(다른 글로 수정됨) (1) | 2024.05.18 |
---|---|
[Compose] Custom CameraView 만들기 (0) | 2024.03.29 |
[Android Tip] Immortal Service (죽지 않는 서비스) (2) | 2023.09.29 |
Application Restart (앱 재실행) (0) | 2023.06.02 |
[Android: Basic] GPS Location Service (위치정보 서비스) (0) | 2023.05.23 |