Go를 이용한 클라우드 네이티브 애플리케이션 레이어 구현과 패턴을 정리하는 시리즈의 시작
앞으로 다룰 많은 예제들은 Go언어에서 제공하는 Context 패키지를 사용하게 되므로 가장 먼저 정리해보려 한다.
Go 1.7 버전에서 처음 소개된 이 패키지는 프로세스 간 종료 시점, 취소 신호 및 요청 범위 값을 전달하기 위한 관용적인 수단을 제공한다.
1. Context 구성
type Context interface {
// Done 메서드는 Context가 취소되었을 때 닫힌 채널을 반환합니다.
Done() <-chan struct{}
// Err 은 Done 메서드를 통해 채널이 닫혔을 때 context가 왜 취소되었는지 나타냅니다.
// 채널이 아직 닫히지 않은 경우 Err은 nil을 반환합니다.
Err() error
// Deadline 은 이 Context가 닫혀야 하는 시간을 반환합니다.
// 종료 시점이 지정되지 않은 경우 ok 값으로 false를 반환합니다.
Deadline() (deadline time.Time, ok bool)
// Value 는 이 context 내에서 key와 연계된 값을 반환합니다.
// key와 연계된 값이 없는 경우 nil을 반환하므로 조심해서 사용해야 합니다.
Value(key interface{}) interface{}
}
Done()
, Err()
, Deadline()
메서드는 Context 값의 취소 또는 동작 상태를 알기 위해 사용하게 된다.
4번째 메서드 Value()
는 임의의 키와 연관된 값을 추출하는데 사용한다.
2. Context가 하는 일
사용자가 유저 데이터를 수정하는 요청을 보냈다고 가정하자.
이상적인 시나리오는 위 그림처럼 사용자, 애플리케이션, DB 요청이 LIFO(Last-In-Firtst-Out) 형식으로 처리되어 결과를 반환한다.
그런데 요청 처리가 완전히 끝나기 전에 사용자가 요청을 중단시키면 어떻게 될까?
대부분의 경우 프로세스는 전반적인 맥락을 알지 못하기 때문에 요청 받은 작업을 계속 처리할 것이고 아무도 사용하지 않을 결과를 반환하기 위해 자원을 낭비할 것이다.
하지만 최상단의 Context를 각 서브 요청들에게 공유하면 장시간 동작해야 하는 프로세스들도 동시에
종료 신호를 받을 수 있게 되므로 각 프로세스들이 취소 신호를 처리하도록 할 수 있다.
중요한 것은 Context 값이 Thread-Safe
하다는 사실이다.
그 덕분에 동시에 실행되는 다수의 고루틴이 특정 Context에 대해 접근하는 경우에도 예측하기 힘든 동작에 대한 걱정 없이 사용할 수 있다.
3. Context 생성
공유를 위한 context.Context는 다음 두 가지 함수를 이용하여 생성할 수 있다.
- func Background() Context
빈 Context 객체를 반환한다. 이 객체는 취소된 상태가 아니고 값이 할당되어 있지 않으며 종료 시점도 갖고 있지 않다.
보통 main 함수나 초기화, Test, 수신되는 요청에 대한 최상위 Context 객체로 사용된다.
- func TODO() Context
이 함수도 빈 Context 객체를 반환하지만 사용되는 시나리오에 차이가 있다.
어떤 Context를 사용해야 할 지 확실하지 않은 경우 또는 부모 Context 객체가 필요하지만 아직 가용하지 않을 때 플레이스 홀더 느낌으로 사용한다.
4. Context 종료 시점과 타임아웃 정의
context 패키지는 취소 관련 동작이 추가된 파생 Context를 만드는 여러 메서드를 제공한다.
타임아웃을 적용하거나 명시적으로 취소를 발생시킬 수 있는 함수를 통해 동작을 정의한다.
- func WithDeadLine(Context, time.Time) (Context, CancelFunc)
Context 를 취소하고 Done 채널을 닫을 특정 시점을 지정한다.
- func WithTimeout(Context, time.Duration) (Context, CancelFunc)
Context를 취소하고 Done 채널이 닫히키 전까지 기다리는 시간을 지정한다.
- func WithCancel(Context) (Context, CancelFunc)
위의 두 함수와 달리 WithCancel은 특별한 값을 받지 않는다.
공통점 : 모두 파생 Context 와 함께 Context를 명시적으로 취소하거나 파생된 모든 값을 취소할 때
사용할 매개변수가 없는 context.CancelFunc
함수를 반환한다.
Context가 취소되면 취소된 Context로부터 파생된 모든 Context들도 취소된다.
5. 요청 범위에 대한 값 정의
context 패키지는 Value 메서드를 통해 반환된 Context와 파생된 모든 Context 값에서 접근할 수 있는
키-값 쌍을 정의하는 함수를 제공한다.
- func WithValue(parent Context, key, val interface{}) Context
키값 쌍을 호출할 수 있는 파생 Context를 만든다.
Value 메서드를 통해서 파생 Context 및 그 아래로 파생되는 모든 Context 값에 접근할 수 있다.
Context Value
context.Withvalue()와 context.Value() 함수는 프로세스나 API가 사용할 수 있는 키-값 쌍을 설정하거나 가져올 수 있는 편리한 구조를 제공한다.
하지만 이 함수는 Context 종료 시점을 정하거나 타임아웃을 정하는 함수와 충돌할 수 있다.
앞으로의 글에서는 이 함수를 사용하지 않는다. 만약 사용하고 싶다면 모든 값들이 요청 내부에서만 한정되어 사용되는지를 확실히 살펴보아야 한다.
6. Context 사용하기
main 함수에 의해서, 또는 외부 요청에 의해서 서비스가 시작되면 최상위 프로세스는
Background()
함수를 이용해 새로운 Context 를 만든다.
그리고 sub요청에게 Context를 공유하기 전에 context.With... 함수들을 이용해서 필요한 값 및 기능을 설정한다.
sub요청들은 Context 를 공유받은 후, 동기화를 위해서 Done 채널을 주시하고 취소 신호가 오는지만 확인하면 된다.
간단한 Stream 함수 예제 코드이다.
func Stream(ctx context.Context, out chan<- string) error {
// 10초 타임아웃을 가진 파생 Context를 생성한다.
// 이렇게 생성된 dctx는 타임아웃 도달 시 취소되지만 ctx는 취소되지 않는다.
// cancel은 dctx를 명시적으로 취소하는 함수이다.
dctx, cancel := context.WithTimeout(ctx, time.Second*10)
// SlowOperation 함수가 타임아웃되기 전에 수행되면 리소스를 반환한다.
defer cancel()
res, err := SlowOperation(dctx)
if err != nil { // dctx가 타임아웃되면 True가 된다.
return err
}
for {
select {
// res를 읽어서 out 채널로 보낸다. (out 채널은 send-only)
case out <- res:
// ctx가 취소되었다는 고루틴 시그널을 받으면 발생한다.
case <-ctx.Done():
return ctx.Err()
}
}
}
func SlowOperation(ctx context.Context) (string, error) {
time.Sleep(11)
return "", nil
}
코드에서 살펴볼 부분은 select 구문 내부의 for 반복문에 ctx 값이 사용되었다는 것이다.
ctx 채널이 닫혔을 때, 적절한 에러 값을 반환하기 위해서 case <- ctx.Done()
구문을 사용한 점이다.
여기까지 Go 동시성 패턴의 핵심인 Context 패키지를 간단하게 정리해보았다.
Ref.
https://product.kyobobook.co.kr/detail/S000201469459
'DevOps > CN' 카테고리의 다른 글
[클라우드 네이티브 패턴] 2. 안정성 패턴 (3) | 2024.01.15 |
---|