티스토리 뷰
📑 context 패키지
Go의 context 패키지는 API의 경계를 넘어, 프로세스 간의 종료 시점, 취소 신호 그리고 요청 범위 값을 전달하는 Context 타입을 정의합니다. 쉽게 말해, Context 타입을 사용하여 작업 흐름을 제어할 수 있습니다.
type Context interface {
// context가 취소되어야 하는 시점을 반환합니다.
// 만약 취소되어야 하는 시점이 없다면, ok는 false를 반환합니다.
Deadline() (deadline time.Time, ok bool)
// context가 취소되었을 때 닫히는 채널을 반환합니다.
// 만약 취소될 수 없는 context라면, Done은 nil을 반환합니다.
Done() <-chan struct{}
// Done이 닫히지 않았다면, Err은 nil을 반환합니다.
// Done이 닫혔다면, Err은 context가 왜 닫혔는지에 대한 non-nil error를 반환합니다.
Err() error
// Value는 key에 해당하는 context의 값을 반환합니다.
// 만약 key에 해당하는 값이 없다면, nil을 반환하므로 주의해야 합니다.
Value(key any) any
}
👷♂️ Context가 하는 일
일반적인 서비스 요청은 다음 그림과 같이 여러 개의 하위 요청과 이에 대한 응답으로 구성되어 있습니다.
그런데 여기서 사용자가 요청 처리가 완전히 끝나기 전에 요청을 중단시킬 수 있습니다. 대부분의 프로세스는 전반적인 맥락을 알지 못하기 때문에 받은 요청을 끝까지 수행하면서 자원을 낭비하게 됩니다.
하지만 Context를 각 하위 요청과 공유하면 Context가 취소됨에 따라 요청을 처리하고 있는 각 프로세스가 동시에 취소 신호를 받아 처리할 수 있습니다.
이때, Context는 스레드 안전하기 때문에 다수의 goroutine에서 동일한 Context를 사용하더라도 예상하지 못한 동작은 거의 발생하지 않습니다.
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
wg := sync.WaitGroup{}
for i := 1; i <= 20; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
// context는 스레드 안전(thread-safe)하므로,
// 여러 개의 goroutine이 동시에 context를 사용해도 안전하다.
<-ctx.Done()
fmt.Printf("goroutine %d done\n", i)
}(i)
}
cancel()
wg.Wait()
}
$ go run main.go
goroutine 20 done
goroutine 10 done
goroutine 11 done
goroutine 12 done
goroutine 13 done
goroutine 14 done
goroutine 15 done
goroutine 16 done
goroutine 17 done
goroutine 18 done
goroutine 19 done
goroutine 4 done
goroutine 1 done
goroutine 2 done
goroutine 6 done
goroutine 5 done
goroutine 7 done
goroutine 8 done
goroutine 3 done
goroutine 9 done
🧩 기본 Context
Background
func Background() Context
빈 Context 객체를 반환합니다. 이 객체는 취소되지 않고, 값이 없으며, 종료 시점도 없습니다. 일반적으로 main 함수, 초기화, 테스트 그리고 수신되는 요청에 대한 최상위 Context로 사용됩니다.
TODO
func TODO() Context
Background와 마찬가지로 빈 Context 객체를 반환합니다. 다만 어떤 Context를 사용해야 할지 정해지지 않은 경우에 사용합니다. 말 그대로 바꿀 예정(TODO)인 셈이죠.
🕐 종료 시점과 타임아웃이 적용된 Context
WithCancel
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
부모 Context의 복사본과 새로운 Done 채널을 가진 객체, 그리고 CancelFunc(취소 함수)를 반환합니다. 명시적으로 Context를 취소할 수 있습니다.
WithTimeout
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
지정된 시간이 지나면 Context를 취소하고 Done 채널을 닫습니다.
WithDeadline
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
지정된 종료 시점이 지나면 Context를 취소하고 Done 채널을 닫습니다.
🕐 요청 범위 값의 정의
WithValue
func WithValue(parent Context, key, val any) Context
key가 val과 연결된 부모 Context의 파생 Context를 반환합니다. 이때 Context를 사용하는 다른 패키지와 충돌을 피하기 위해 key는 내장 string 타입이 아닌 사용자가 정의한 타입을 사용하는 것을 권장합니다.
❓ Context 취소 이유
Cause
func Cause(c Context) error
1.20 업데이트에 추가된 함수로, 왜 Context가 취소되었는지를 반환합니다. CancelCauseFunc(err)을 사용해서 취소된 Context의 경우, err을 반환하며 그렇지 않다면 c.Err()과 동일한 결과를 반환합니다. c가 취소되지 않았다면 nil을 반환합니다.
WithCancelCause
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc)
1.20 업데이트에 추가된 함수로 WithCancel 함수와 비슷하게 동작하지만, CancelFunc대신 CancelCauseFunc를 반환합니다. CancelCauseFunc(err)을 사용하여 err을 지정할 수 있으며, Cause(ctx)를 사용해 err을 회수할 수 있습니다.
WithDeadlineCause
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc)
1.21 업데이트로 추가된 함수로 WithDeadline과 비슷하게 동작하나, 함수를 호출할 때 Context가 취소된 이유를 지정할 수 있습니다.
WithTimeoutCause
func WithTimeoutCause(parent Context, timeout time.Duration, cause error) (Context, CancelFunc)
1.21 업데이트로 추가된 함수로 WithTimeout과 비슷하게 동작하나, 함수를 호출할 때 Context가 취소된 이유를 지정할 수 있습니다.
예시
package main
import (
"context"
"errors"
"fmt"
)
func main() {
ctx, cancel := context.WithCancelCause(context.Background())
cancel(errors.New("piatoss canceled context"))
cause := context.Cause(ctx)
fmt.Println(cause)
fmt.Println(ctx.Err())
fmt.Println(errors.Is(cause, ctx.Err()))
}
$ go run main.go
piatoss canceled context
context canceled
false
🦹♂️ Context가 종료된 뒤
AfterFunc
func AfterFunc(ctx Context, f func()) (stop func() bool)
AfterFunc는 Context가 종료된 뒤에 자체 goroutine으로 f를 실행합니다. 만약 Context가 이미 종료된 상태라면 즉시 goroutine으로 f를 실행합니다. Context에 대해 AfterFunc 함수로 등록한 f 함수는 독립적으로 실행됩니다.
예시
package main
import (
"context"
"fmt"
"sync"
)
func main() {
wg := sync.WaitGroup{}
wg.Add(2)
f1 := func() {
defer wg.Done()
fmt.Println("f1() called")
} // f1의 종료 시점을 알기 위해 명시적인 조정이 필요하다. 따라서 sync.WaitGroup을 사용한다.
f2 := func() {
defer wg.Done()
fmt.Println("f2() called")
}
ctx, cancel := context.WithCancel(context.Background())
context.AfterFunc(ctx, f1) // AfterFunc를 사용하여 f1을 등록한다.
context.AfterFunc(ctx, f2) // AfterFunc를 사용하여 f2를 등록한다.
cancel() // ctx가 종료되면 f1과 f2가 독립적인 goroutine으로 실행된다.
wg.Wait() // f가 종료될 때까지 대기한다.
}
$ go run main.go
f2() called
f1() called
🙏 마치며
오늘은 context 패키지와 그 역할에 대해 알아보았습니다. 1.20 업데이트 이후로 Context가 취소된 이유를 추가할 수 있는 함수가 추가되어서 한층 개선된 트러블슈팅이 가능하게 된 것 같아 흥미롭습니다.
go를 사용하면서 context 패키지가 사용되는 경우를 굉장히 많이 봤는데, 솔직히 그냥 다들 사용하니까 나도 해야 될 것 같은 느낌으로 사용하고 있었습니다. 그러다 오늘 context가 작업 흐름을 관리한다는 사실을 마주하고 보니 진작에 정리를 해볼걸 하는 생각이 드네요. 어떤 목적으로 사용하는지 정확하게 알고 사용하는 것과 그렇지 않은 것에 극명한 차이가 존재하는 것 같습니다.
📖 참고자료
글에서 수정이 필요한 부분이나 설명이 부족한 부분이 있다면 댓글로 남겨주세요!
'Go > 문서 읽기' 카테고리의 다른 글
Go 1.22.0 업데이트 살펴보기 (0) | 2024.02.08 |
---|---|
[Go] Vault를 사용해 시크릿 저장하고 불러오기 기초 (0) | 2023.10.25 |
[Effective Go] Names (0) | 2023.03.29 |