Go 언어 시간 관리의 두 마법사: Timer와 Ticker 완벽 이해!

Go 언어 시간 관리의 두 마법사: Timer와 Ticker 완벽 이해!

Go 프로그래밍의 세계에 발을 들여놓으셨다면, 특정 시간이 지난 후에 어떤 작업을 실행시키고 싶거나, 혹은 주기적으로 반복되는 작업을 처리해야 하는 순간들을 만나게 됩니다.

마치 알람 시계처럼, "이따가 알려줘!" 또는 "매시간 알려줘!" 같은 요구사항들 말입니다.

Go 언어는 이러한 시간 관련 작업들을 아주 깔끔하고 효율적으로 처리할 수 있도록 time 패키지 안에 특별한 도구들을 마련해 두었는데요.

그 주인공은 바로 Timer(타이머)Ticker(티커)입니다.

오늘은 이 두 가지 시간 관리의 마법사들이 각각 어떤 역할을 하고, 어떻게 사용해야 하는지, 그리고 사용할 때 주의할 점은 무엇인지 쉽고 재미있게 알아보도록 하겠습니다.

1. 단 한 번의 알림, Timer(타이머) 파헤치기

Timer(타이머)는 이름에서 알 수 있듯이, 딱 한 번의 시간 지연 후 이벤트를 발생시키는 데 사용됩니다.

예를 들어 "2초 후에 이 함수를 실행해줘"와 같은 요청을 처리할 때 아주 유용합니다.

Timer(타이머)는 설정된 시간이 지나면 자신의 채널(C)을 통해 현재 시간을 보내주는 방식으로 작동하는데요.

코드를 통해 직접 확인해 볼까요?

package main

import (
    "fmt"
    "time"
)

func main() {
    // 2초 후에 신호를 보낼 타이머 생성
    timer := time.NewTimer(2 * time.Second)

    // 타이머의 C 채널로부터 신호를 기다림
    // 2초가 지나면 이 부분의 대기가 풀림
    <-timer.C
    fmt.Println("타이머 만료! 2초가 지났습니다.")
}



위 예제에서는 time.NewTimer() 함수를 사용해 2초짜리 Timer(타이머)를 만들었습니다.

그리고 <-timer.C 부분을 통해 Timer(타이머)가 신호를 보낼 때까지 프로그램은 잠시 멈춰 기다리게 됩니다.

정확히 2초가 지나면 timer.C 채널로 신호가 전달되고, 프로그램은 다시 실행되어 "타이머 만료!" 메시지를 출력하게 되는 것이지요.

만약 설정한 시간이 되기 전에 Timer(타이머)를 멈추고 싶다면 어떻게 해야 할까요?

그럴 때는 Stop() 메소드를 사용하면 됩니다.

Stop() 메소드는 Timer(타이머)가 성공적으로 멈췄는지 여부를 불리언(boolean) 값으로 반환해 주는데요.

if timer.Stop() {
    fmt.Println("타이머가 만료되기 전에 멈췄습니다.")
}




이렇게 하면, 아직 알람이 울리기 전에 "아, 그 알람 취소!" 하고 외치는 것과 같다고 생각할 수 있습니다.

2. 주기적인 알림, Ticker(티커) 마스터하기

Timer(타이머)가 단 한 번의 알람이라면, Ticker(티커)는 마치 심장 박동처럼 일정한 간격으로 계속해서 신호를 보내주는 역할을 합니다.

"1초마다 현재 시간을 알려줘"와 같이 주기적인 작업이 필요할 때 Ticker(티커)는 최고의 선택이 될 수 있는데요.

Ticker(티커) 역시 자신의 채널(C)을 통해 주기적으로 현재 시간을 전달합니다.

코드를 통해 Ticker(티커)의 작동 방식을 살펴보겠습니다.

package main

import (
    "fmt"
    "time"
)

func main() {
    // 1초 간격으로 신호를 보내는 티커 생성
    ticker := time.NewTicker(1 * time.Second)
    // 티커를 멈추기 위한 신호를 받을 채널 생성
    done := make(chan bool)

    // 고루틴(goroutine)을 사용하여 5초 후에 done 채널로 true를 보냄
    go func() {
        time.Sleep(5 * time.Second)
        done <- true
    }()

    // 무한 루프를 돌면서 티커 또는 done 채널의 신호를 기다림
    for {
        select {
        // 티커의 C 채널로부터 신호를 받으면 현재 시간 출력
        case t := <-ticker.C:
            fmt.Println("똑딱! 현재 시간:", t)
        // done 채널로부터 신호를 받으면 티커를 멈추고 함수 종료
        case <-done:
            ticker.Stop()
            fmt.Println("티커가 멈췄습니다.")
            return
        }
    }
}



이 예제에서는 1초마다 "똑딱!"하고 신호를 보내는 Ticker(티커)를 만들었습니다.

동시에, 5초 후에 Ticker(티커)를 멈추기 위한 done 채널을 준비했는데요.

for 루프 안의 select 문은 ticker.C 채널과 done 채널 중 어느 쪽에서든 먼저 신호가 오는 것을 기다립니다.

ticker.C에서 신호가 오면 현재 시간을 출력하고, 5초가 지나 done 채널에서 신호가 오면 ticker.Stop()을 호출하여 Ticker(티커)를 멈추고 프로그램을 종료하게 됩니다.

3. Timer(타이머)와 Ticker(티커), 내부에서는 어떻게 움직일까요?

그렇다면 Go 언어는 어떻게 이렇게 정확하게 시간을 맞춰 Timer(타이머)Ticker(티커)를 작동시킬 수 있는 걸까요?

사실 Go 언어의 런타임(runtime), 즉 프로그램 실행 환경 내부에는 시간 관련 이벤트들을 아주 효율적으로 관리하는 정교한 시스템이 마련되어 있습니다.

runtimeTimer라는 내부 구조체를 중심으로, 언제 이벤트가 발생해야 하는지(when 필드), 만약 반복적인 이벤트라면 그 주기는 어떻게 되는지(period 필드) 등의 핵심 정보를 관리하는데요.

그리고 startTimer, stopTimer, resetTimer와 같은 내부 함수들이 이 타이머들을 정밀하게 제어하며 우리가 사용하는 Timer(타이머)Ticker(티커)의 기능을 구현합니다.

더 깊이 있는 작동 원리가 궁금하다면 Go 언어의 공식 소스 코드를 살펴보는 것도 좋은 공부가 될 수 있지만, 일단은 "Go가 내부적으로 알아서 잘 처리해준다!" 정도로 이해하고 넘어가도 충분합니다.

4. Timer(타이머)와 Ticker(티커), 이것만은 기억하고 사용합시다!

Timer(타이머)Ticker(티커)는 매우 유용한 도구이지만, 몇 가지 주의사항을 기억하고 사용해야 더욱 안전하고 효율적인 프로그래밍이 가능한데요.

자원 관리는 철저하게!

가장 중요한 것은 더 이상 사용하지 않는 Timer(타이머)Ticker(티커)는 반드시 Stop() 메소드를 호출해서 명시적으로 멈춰줘야 한다는 점입니다.

그렇지 않으면 해당 Timer(타이머)Ticker(티커)가 계속해서 시스템 자원을 불필요하게 차지하게 되어, 프로그램 전체의 성능에 좋지 않은 영향을 줄 수 있습니다.

"다 쓴 알람 시계는 꺼두기!" 꼭 기억해야 합니다.

채널 작업 시 고루틴(Goroutine) 누수 주의!

Go 언어의 강력한 동시성 처리 기능인 고루틴(goroutine)과 함께 Timer(타이머)Ticker(티커)를 사용할 때는 특히 '고루틴 누수(goroutine leak)'를 조심해야 합니다.

Timer(타이머)Ticker(티커)의 채널로부터 신호를 기다리는 고루틴이, 신호를 받지 못하거나 혹은 다른 이유로 제대로 종료되지 못하고 계속 남아있게 되면, 마치 물이 새는 것처럼 시스템 자원이 조금씩 고갈될 수 있습니다.

따라서 채널을 사용하는 고루틴이 항상 적절한 시점에 깔끔하게 종료될 수 있도록 프로그램을 설계하는 것이 매우 중요합니다.

지금까지 Go 언어에서 시간과 관련된 작업을 정밀하고 효율적으로 처리할 수 있도록 도와주는 Timer(타이머)Ticker(티커)에 대해 자세히 알아보았습니다.

이 두 가지 도구를 상황에 맞게 잘 활용한다면, 여러분의 Go 프로그램은 한층 더 강력하고 안정적으로 작동하며 시간이라는 요소를 자유자재로 다룰 수 있게 될 것입니다.