Go 언어 time.Timer#Reset()
완벽 가이드: 올바른 사용법 알아볼까요?
Go 언어에서 time.Timer
를 사용하고 계신가요?
매우 기본적인 요소인데도 올바르게 사용하는 것은 의외로 어렵습니다.
얼마 전에 직접 겪은 사례와 함께 그 올바른 사용법을 소개하려고 합니다.
TL;DR
time.Timer#Stop()
후에는time.Timer.C
를 읽어서 남은 값을 버려야 할 때가 있습니다.ti := time.NewTimer(5 * time.Second) // ... if !ti.Stop() { <-ti.C }
time.Timer#Reset()
은 타이머가 정지된 상태에서 호출해야 합니다.// ... if !ti.Stop() { <-ti.C } ti.Reset(3 * time.Second) // 3초 더 기다립니다.
- 타이머에 대한 이러한 조작은 동시에 실행하면 안 됩니다.
Stop()의 올바른 사용법
앞서 보신 것처럼 time.Timer
의 Stop()
을 호출할 때의 올바른 방법은 다음과 같습니다.
if !ti.Stop() {
<-ti.C
}
Stop()
의 반환값은 이 호출로 타이머가 멈췄는지 여부를 나타냅니다.
이미 멈춰있다면 false
를 반환합니다.
이때, 타이머는 이미 발행되었고 C <-chan Time
에 데이터가 들어 있습니다. 이를 소비하여 남은 값을 버려주어야 합니다.
그렇지 않으면 예상치 못한 동작이 발생할 수 있습니다.
Reset()의 올바른 사용법
Reset()
은 타이머의 시간을 재설정하는 메서드입니다. 사용 예시는 다음 두 가지가 있습니다.
- 이미 발행된 타이머를 재사용하기
- 아직 발행되지 않은 타이머의 시간을 재설정하거나 연장하기
특히 두 번째 경우, 현재 동작 중인 타이머를 일단 멈추고 새로운 시간을 설정합니다.
실제로 Reset()
내부에서는 저수준 타이머를 멈추는 절차가 있지만, Reset()
을 호출하고 저수준 타이머가 멈출 때까지의 사이에 발행될 수 있습니다.
이를 무시하면 지정된 시간이 지나지 않았는데도 타이머가 발행된 것처럼 되어버리니, 다음과 같은 코드로 확실히 멈춰주어야 합니다.
if !ti.Stop() {
<-ti.C
}
ti.Reset(3 * time.Second) // 3초 더 기다립니다.
하지만 이 코드를 첫 번째 사용 예시인 이미 발행되었고 ti.C
를 이미 소비한 경우에 사용하면 <-ti.C
부분에서 대기 상태에 빠질 수 있습니다.
특히 타이머 발행을 별도의 고루틴에서 감시하는 경우에 주의해야 합니다.
잘못된 코드 예시:
ti := time.NewTimer(5 * time.Second)
go func() {
<-ti.C
// 타이머가 발행되었을 때 처리...
}()
// ... 타이머의 배경에서 작업 수행 ...
// 어떤 조건에서 타이머를 3초 연장
if someCondition {
if !ti.Stop() {
<-ti.C // 여기서 대기 상태에 빠질 수 있음
}
ti.Reset(3 * time.Second)
}
위 코드는 잘못된 예시이니 그대로 복사해서 사용하지 마세요.
올바른 타이머 Reset()
조작은 다음과 같습니다.
ti := time.NewTimer(5 * time.Second)
go func() {
<-ti.C
// 타이머가 발행되었을 때 처리...
}()
// ... 타이머의 배경에서 작업 수행 ...
// 어떤 조건에서 타이머를 3초 연장
if someCondition {
if !ti.Stop() {
select {
case <-ti.C:
default:
}
}
ti.Reset(3 * time.Second)
}
동시 조작 금지
여기까지 time.Timer
를 살펴봤는데요, 비슷한 기능으로 time.Ticker
가 있습니다.
Ticker
는 일정한 간격으로 반복 발행하는 타이머지만, 발행 간격 면에서는 유연성이 부족합니다.
예를 들어 30초 간격으로 반복되는 작업을 특정 시점에서 30초 연장하고 싶을 때, 구체적으로는 KeepAlive의 PING을 보내다가 다른 작업을 하면 PING 타이머를 리셋하는 용도로는 Ticker
를 사용할 수 없습니다.
하지만 Timer
라면 가능합니다.
여기에 비동기적인 Stop()
조작을 더하면 더욱 복잡해집니다.
앞서 보았듯이 time.Timer
에 대한 동시 조작은 예상치 못한 동작을 유발할 수 있습니다.
따라서 동시 조작을 하지 않는 것이 중요하며, 공식 문서에도 다음과 같이 기재되어 있습니다.
time.Timer#Reset
문서 중:- This should not be done concurrent to other receives from the Timer’s channel.
타이머의 채널에서 다른 수신과 동시에 수행해서는 안 됩니다.
time.Timer#Stop
문서 중:- This cannot be done concurrent to other receives from the Timer’s channel.
타이머의 채널에서 다른 수신과 동시에 수행할 수 없습니다.
이를 고려하면 타이머에 대한 조작은 하나의 고루틴에 모아서 처리하는 것이 좋을 것 같습니다.
조금 작성해보면 다음과 같습니다.
// 타이머의 생명주기를 관리
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var mu sync.Mutex
rst := make(chan time.Duration)
go func() {
ti := time.NewTimer(30 * time.Second)
running := true
stop := func() {
if running && !ti.Stop() {
<-ti.C
}
running = false
}
for {
select {
case <-ctx.Done():
mu.Lock()
close(rst)
rst = nil
mu.Unlock()
stop()
return
case d := <-rst:
stop()
ti.Reset(d)
running = true
case <-ti.C:
running = false
// 타이머가 발행되었을 때 처리...
}
}
}()
reset := func(d time.Duration) {
mu.Lock()
if rst != nil {
rst <- d
}
mu.Unlock()
}
// cancel() - 타이머 취소
// reset(30 * time.Second) - 30초 후에 다시 타이머 발행
이 예제에서는 Stop()
시에 select
으로 <-ti.C
를 감싸는 대신 변수 running
으로 가드를 해보았습니다.
타이머에 대한 접근이 고루틴 내부로 모였기 때문에 간단한 방법으로도 충분합니다.
정리하며
아주 기본적인 time.Timer
지만, 제대로 사용하려면 고려해야 할 점이 많다는 것을 알게 되었습니다.
방심하지 말고 항상 문서를 꼼꼼히 읽어 적절한 코드를 작성해야 합니다.
'Go' 카테고리의 다른 글
Go 언어에서 Templ 또는 일반 템플릿: 무엇을 선택해야 할까요? (0) | 2024.09.19 |
---|---|
Go 언어에서 구조체 필드를 반드시 지정하여 초기화하는 방법 알아볼까요? (0) | 2024.09.19 |
Go 언어에서 chan chan 즉, 채널을 채널로 주고받기 (0) | 2024.09.19 |
Go 실행 파일에 ZIP으로 리소스 임베딩하기: 간단하게 알아볼까요? (0) | 2024.09.19 |
Go 언어 fmt.Printf 완벽 가이드 (0) | 2024.09.19 |