Go Lang/Study

[GoLang] 시간 초과 로직 Context, 채널과 비교

DSeung 2025. 2. 25. 15:09

개요

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이 용이하네

반응형