본문 바로가기
Concepts/Cloud Native

Debounce

by ocwokocw 2023. 10. 30.

- 출처: 클라우드 네이티브 패턴

 

- 개요

Debounce는 함수 호출 빈도를 제한하여 여러 번 호출 발생시 처음이나 마지막 호출만 동작하도록 하는 패턴이다.


- Context and Problem

시스템의 작업중에서는 속도가 느리고 비용이 많이 드는 작업이 존재한다. 이런 유사한 작업이 연속적으로 여러 번 발생할때마다 서버가 요청을 처리하면 무거운 작업을 계속 수행해야 한다.


- Solution

Front-end 개발을 해본적이 있다면 Debounce나 Throttle이라는 용어가 익숙할것이다. 가장 대표적인 예제가 자동완성검색이다. 이를 구현할 때 검색창에 입력 이벤트가 발생할때마다 요청을 서버로 보내면, 서버의 부하가 많이 걸릴 수 있다. 그래서 Debounce 패턴을 이용하여 일정 기간내의 마지막 호출 상태의 검색 text만 서버가 요청을 받는식으로 구현한다.

 

Debounce의 구현은 circuit breaker가 보호하려고 하는 로직에 빈도제한을 적용한것과 유사한 형태가 된다. 시간 간격이 지나기 전에 들어온 요청은 무시되며 이후에 들어온 요청만 내부로직으로 전달된다. 최초 호출만 수행하고 이후 요청을 무시하는 형태를 Function-First 라고 하며, 반대의 형태로 동작하는 형태를 Function-Last 라고 한다. Function-Last의 경우 구현이 복잡하기 때문에 이 글에서는 Function-First 형태만 알아보도록 한다.


- Golang example

golang의 Debounce 라이브러리가 몇개 있긴 하지만 서버 구현시 Debounce 패턴을 많이 사용하지 않아서 인지 아니면 구현이 간단해서 인지 모르겠으나 Star 수가 적다. 따라서 Function-First 방식을 구현하여 동작을 확인한다.

테스트 시나리오는 아래와 같다.

  • Atomic counter를 선언하고 Debounce가 실행할 함수에서 counter 를 1 만큼 증가
  • 10번 반복문 호출, 호출마다 counter의 값이 1인지 확인
  • 설정 duration(여기에서는 3초) 동안 sleep
  • 10번 반복문 호출, 호출마다 counter의 값이 2인지 확인

우선 Debounce 구현 코드 부터 살펴보자.

type ExecFunc func() (interface{}, error)

func DebounceFirst(exec ExecFunc, d time.Duration) ExecFunc {
	var threshold time.Time
	var result interface{}
	var err error
	var m sync.Mutex

	return func() (interface{}, error) {
		m.Lock()

		defer func() {
			threshold = time.Now().Add(d)
			m.Unlock()
		}()

		if time.Now().Before(threshold) {
			return result, err
		}
		result, err = exec()
		return result, err
	}
}
  • threshold는 특정시간까지 들어온 요청을 무시하고 이전에 caching한 결과를 반환할 시점을 설정한다. 
  • result, err은 caching할 결과를 담아둘 변수이다. result의 경우 string이나 특정 type을 사용해도 상관없다.
  • m 은 mutex를 사용하기 위함인데, 순차 실행을 보장해야 최초 호출 실행결과를 캐싱하고 이후 호출부터는 threshold 시점 미만일 때 캐싱된 결과 반환할 수 있기 때문이다.

 

아래는 Test 함수이다.

func TestDebounce(t *testing.T) {
	duration := 3 * time.Second
	counter := atomic.Int64{}
	const (
		numOfSection = 2
	)

	execFunc := DebounceFirst(func() (interface{}, error) {
		counter.Add(1)
		return counter.Load(), nil
	}, duration)

	for expectedCounterVal := 1; expectedCounterVal <= numOfSection; expectedCounterVal += 1 {
		for i := 0; i < 10; i += 1 {
			cnt, err := execFunc()
			require.NoError(t, err)
			require.Equal(t, int64(expectedCounterVal), cnt)
		}
		time.Sleep(duration)
	}
}

단락 초반에 언급한 시나리오 대로 Test를 수행한다.

 

Debounce 구현에서 굳이 실행함수 type을 func() (interface{}, error)로 선언한 이유가 있는데, 이는 Circuit Breaker 글(https://ocwokocw.tistory.com/317) 에서 사용해본 circuit breaker library의 실행 함수와 type을 맞추어준것이다. 이렇게 type을 맞추어주면 아래와 같이 Debounce를 Circuit Breaker와 함께 사용할 수 있는 장점이 있다.

func TestDebounceWithCircuitBreaker(t *testing.T) {
	cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{
		Name:          "with debouncer",
		MaxRequests:   5,
		Interval:      5 * time.Second,
		Timeout:       5 * time.Second,
		ReadyToTrip:   nil,
		OnStateChange: nil,
		IsSuccessful:  nil,
	})

	duration := 3 * time.Second
	counter := atomic.Int64{}
	debounceExec := DebounceFirst(func() (interface{}, error) {
		counter.Add(1)
		return counter.Load(), nil
	}, duration)
	const numOfSection = 2
	for expectedCounterVal := 1; expectedCounterVal <= numOfSection; expectedCounterVal += 1 {
		for i := 0; i < 10; i += 1 {
			cnt, err := cb.Execute(debounceExec)
			require.NoError(t, err)
			require.Equal(t, int64(expectedCounterVal), cnt)
		}
		time.Sleep(duration)
	}
}

 

'Concepts > Cloud Native' 카테고리의 다른 글

Timeout  (1) 2023.11.11
Throttling  (0) 2023.11.04
Retry  (0) 2023.10.31
Circuit Breaker  (0) 2023.10.27
Cloud Native의 구성요소  (0) 2023.10.10

댓글