[Go] 컨텍스트(Context) 본문

고루틴은 한 번 시작하면 외부에서 직접 멈출 방법이 없다. 채널로 quit 신호를 보내는 방법이 있지만, 고루틴이 다른 고루틴을 생성하는 계층 구조에서 quit을 일일이 전달하는게 너무 복잡해진다.
context는 취소 신호를 트리 구조로 자동 전파하기 때문에 복잡한 계층 구조에서 효과적으로 고루틴을 정지시킬 수 있다.

위의 그림처럼 고루틴이 부모 자식 관계로 트리 구조를 형성해 있을 때, 문제가 발생하면 context를 통해 취소 신호가 전파된다. 이를 실제로 구현하면 아래의 코드와 같다.

HTTP Handler에서 파생되는 queryDB 함수와 queryCache 함수, callAPI() 함수의 파라미터로 컨텍스트를 전달하여 취소 신호가 전파될 수 있도록 한다.
context에는 자주 사용되는 4가지 생성 함수가 존재한다.
context.Background()는 루트 컨텍스트로 main함수, 테스트, 최상위 고루틴에서 사용되는 절대 취소되지 않는 빈 컨텍스트다. 모든 컨텍스트 트리의 시작점 역할을 한다.

빈 컨텍스트를 하위 함수에 전달하면서 필요에 따라 WithCancel() 등으로 래핑할 수 있다.
context.WithCancel()는 작업이 끝났을 때 혹은 에러가 났을 때 등 수동으로 고루틴을 취소하고 싶을 때 사용된다.
사용 시 cancel()을 호출하지 않으면 부모 고루틴이 취소될 때까지 자식 컨텍스트의 리소스가 해제되지 않기 때문에 defer cancel()을 필수적으로 사용해야 한다.

context.WithTimeout()은 N초 안에 응답을 받지 못하면 동작을 취소하고 싶을 때 사용한다. HTTP 요청 혹은 DB 쿼리를 호출할 때 자주 사용된다.
WithTimeout(ctx, d)은 내부적으로 WithDeadline(ctx, time.Now().Add(d))와 동일하기 때문에 지정한 시간이 지나면 cancel()을 자동으로 호출한다.
하지만 defer cancel()을 명시하는 편이 좋다. 시간이 지나면 컨텍스트는 취소되지만, 고루틴에 지정된 타임아웃의 지정 시간과 리소스는 자동으로 해제되지 않기 때문에 호출하지 않으면 내부 타이머와 타이머가 참조하는 메모리가 지정된 시간이 지날 때까지 계속 할당되어 있기 때문에 리소스가 낭비된다.

context.WithValue()는 요청 ID, 인증 토큰처럼 요청 범위의 데이터를 파라미터 추가 없이 전달하려고 할 때 주로 사용된다.
WithValue()를 통해 전달되는 키로 string, int 같은 내장 타입을 쓰게 되면 다른 패키지의 키와 충돌이 발생할 수 있기 때문에 반드시 패키지 전용 struct 타입을 키로 사용해야 한다.
예를 들어, 키 값으로 "userID"를 전달하게 되면 다른 패키지에서도 같은 키로 접근이 가능하기 때문에 충돌 가능성이 생긴다.
그래서 패키지마다 전용 구조체 타입을 만들어 외부에서 접근이 불가능하게끔 만들거나 구분될 수 있도록 만들 필요가 있다.
Go는 타입을 비교할 때 패키지 경로까지 포함해서 판단하기 때문에, 이름이 같아도 다른 패키지에 위치해 있다면 서로 다른 타입으로 구분된다.

구현한 구조체를 unexported 하게끔 하여 외부에서 직접 건드리지 못하도록 하는 대신 Set/Get 함수를 필요에 따라 구현하여 전달하는 것이 좋다. 이는 Java의 캡슐화와 굉장히 유사하기 때문에 이전에 Java를 했던 사람이라면 무슨 말인지 쉽게 이해할 수 있을 것이다.
컨텍스트는 사용함에 있어 몇 가지 사용 규칙을 잘 준수해야 버그가 발생하지 않기 때문에 올바른 사용 방법을 숙지하는 것이 중요하다.
컨텍스트를 전달할 때는 첫 번째 파라미터로 전달하는 것이 올바른 사용법이며, 컨텍스트를 구조체에 저장하거나 nil 컨텍스트를 전달하지 않아야 한다. 만약 빈 컨텍스트를 전달하고 싶다면 context.TODO()를 전달하는 것이 좋다. 그리고 한 번 더 언급하자면 defer cancel()을 명시하자.
주로 select문과 context를 연계하여 사용되기 때문에 이전 포스팅에 작성했던 내용을 참고하여 두 기능 모두 잘 공부해서 사용하도록 하자.

Go를 시작한 계기는 쿠버네티스가 Go로 짜여져 있다는 말을 듣기도 했고 OpenTelemetry 사용 시 OCB를 사용하려면 Go로 작성해야 한다는 말을 들어서 였는데, 하다보니 Go라는 언어 자체에 매력이 있는 것 같다.
Go를 사용해서 메트릭 수집기를 만드는 프로젝트를 진행하고 있는데 관련된 내용도 기회가 된다면 포스팅 해보려고 한다.
'Dev' 카테고리의 다른 글
| [Go] 고루틴(Goroutine) & 채널(Channel) (0) | 2026.03.27 |
|---|---|
| [Go] 메모리 관리에 대해서 알아보자 (0) | 2026.03.18 |
| 단일 트랜잭션 유지 그리고 PGMQ (2) | 2024.12.15 |
| [Go] 함수 고급 (0) | 2024.12.02 |
| [Go] 인터페이스 (5) | 2024.11.10 |