개요
go의 context 기능은 go 커뮤니티에서도 논쟁이 많고 이야기가 여러 가지가 있습니다.
정리하면 주로 아래와 같죠
- 작업 취소와 타임 아웃의 유용성 : 고루틴 작업에서의 취소와 타임아웃에서는 리소스 누수 방지와 응답 개선의 효과가 있음
- 사용에 대한 어려움 : 초기 학습의 혼란과 올바른 패턴에 이해 필요
- 매개변수 남용 우려 : context로 값을 전달하는 과정이 오히려 가독성을 해칠 수 있다
- 로깅과 통합 : 고유 값을 context에 담아서 로깅에 사용, 매개변수 남용이 될 수 있음
동시성에서 필요하고 쓰기는 좋은 건 맞지만, 올바른 패턴의 필요성을 강조하고 있습니다.
남발하면 가독성을 그만큼 해쳐지기 때문이죠
저 같으 경우 아직 Context의 존재와 기능은 머리로는 알고 있습니다만
회사 소스 중에 어디에 적용하기가 애매해서 보류하고 있습니다.
그나마 적용하기 좋은 부분은 시간초과로 생각하고 있습니다.
이 부분이 Context의 기능 중에서 가장 직관적입니다.
코드
3초가 지나면 타임아웃이 울리는 로직에서 작업이 5초간 진행될 때를 가정한 코드입니다.
그걸 Context로 푼 코드죠
package main
import (
"context"
"fmt"
"time"
)
func process() {
// 3초 후 종료하는 컨텍스트
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
// 작업 수행
err := doSomething(ctx)
// 타임 아웃에 따른 에러 반환
if err != nil {
fmt.Println("Status Request Timeout")
} else {
// 정상 처리
fmt.Println("Success")
}
}
func doSomething(ctx context.Context) error {
fmt.Println("Processing request...")
select {
// 5초 작업
// select 문과 같이 사용해야지, 컨텍스트 취소를 감지 가능
case <-time.After(5 * time.Second):
fmt.Printf("Task completed successfully")
return nil
case <-ctx.Done(): // context 에 종료 신호가 올 경우
fmt.Println("Task aborted due to timeout:", ctx.Err())
return fmt.Errorf("Request Timeout")
}
}
func main() {
process()
}
이걸 실행하면 다음과 같이 context deadline으로 에러가 뜨면 원하던 바를 이룬 거죠
Processing request...
Task aborted due to timeout: context deadline exceeded
Status Request Timeout
이제 실제 서비스 로직에서도 데이터 처리를 하면서 응답이 길어질 때를 대처할 수 있게 되었습니다.
하지만 의문이 든 점은 채널로도 충분히 커버가 가능하다는 생각이 들었습니다.
아래는 같은 코드를 채널로 구현했을 때죠.
package main
import (
"fmt"
"time"
)
func process() {
// 작업 완료 여부를 전달할 채널
done := make(chan error)
// 작업 수행
go doSomething(done)
// 타입 아웃 채널
timeOut := time.After(3 * time.Second)
select {
case err := <-done: // 작업 완료
if err != nil {
fmt.Println("Status Request Timeout")
} else {
fmt.Println("Success")
}
case <-timeOut: // 3초 후 타임아웃
fmt.Println("Status Request Timeout")
}
}
func doSomething(done chan<- error) {
fmt.Println("Processing request...")
select {
case <-time.After(5 * time.Second): // 5초 걸리는 작업
fmt.Println("Task completed successfully")
done <- nil // 작업 성공 신호 전송
}
}
func main() {
process()
}
결과는 아래와 같죠
Processing request...
Status Request Timeout
context 에러를 찍는 부분이 사려져서 결과가 살짝 다르지만, 같은 로직을 구현했습니다.
채널로도 충분히 비슷한 기능을 구현할 수 있지만, 아래 사항들을 직접 구현해야 했네요
- 채널로 취소 전파 구현
- 채널 동기화 구현, 확장 시 계속 동기화 로직이 추가됨
- 채널의 값을 직접 핸들링해야 함
확실히 다소 귀찮아 짐을 알 수 있습니다.
Context와 Channel의 차이를 표로 하면 다음과 같죠
context.Context 사용 | channel 사용 | |
코드 복잡도 | ✅ 간단 (ctx.Done()) | ❌ 타이머, 채널 직접 관리 필요 |
취소 전파 | ✅ 자동 | ❌ 직접 채널로 취소 구현 |
타임아웃 처리 | ✅ WithTimeout 사용 | ❌ time.After로 직접 설정 |
확장성 | ✅ 부모-자식 컨텍스트 활용 가능 | ❌ 추가적인 채널 동기화 필요 |
확실히 취소 전파, 타임아웃, 확장이나, 매개변수를 담을 수 있다는 점 등
context의 기능이 다채롭다 보니 잘하면 잘 쓸 수 있을 것 같습니다.
당장 적용시켜 볼 분야는 로깅과 시간초과 등이 있겠군요
다른 분의 예제를 볼 때는 grpc에서 데이터를 받으면 이를 context로 핸들링해서 관련 함수 및 키-저장 변수를 사용하더군요
그리고 컨텍스트를 까보면 채널과 인터페이스를 구현된거라, 채널과 인터페이스로 대체가 가능한건 어찌보면 당연한거네요
하지만 Context를 사용함으로써 값의 저장소의 생명 주기(맥락)를 관리한다.
그래서 맥락이라고 하는 것 같군요.
※ gin에 있는 Context는 이것과 달리, request 핸들링에 특화된 다른 기능입니다 (타 언어의 context 형태)
이 부분이 재밌네요. 정확히는 다른 기능을 하는데, 키워드는 같다는 점이 상당히 마음에 안드네요. ※
추가로 Context의 deadline과 timeout을 비교했을 때 deadline의 쓰임새가 붕 뜨는 느낌이었는데
생각해보니 은행권같이 11시 55분부터 점검을 해야해서 모든 요청을 막아야할 때 이럴 때는 deadline이 용이하네
'Go Lang > Study' 카테고리의 다른 글
[GoLang] net/http에서 http 요청을 동시적으로 처리하는 이유 (0) | 2025.02.17 |
---|---|
[GoLang] GC 튜닝 Profiling (0) | 2025.02.05 |
[GoLang] Garbage Collector 개념 (0) | 2025.01.29 |
[GoLang] 추상 팩토리 디자인 사용해보기 (1) | 2025.01.17 |
[GoLang] GraphQL API 만들기 part 1 (라이브러리 탐색) (38) | 2024.03.01 |