
Datadog 의 trace를 연결하던 도중, 외부 API 로 Request할 때의 trace도 Context를 이용하여 trace 흐름을 연결시킬 수 있다고 하여.. Context 개념 좀 다시 알아야겠다 싶어서 ( 결론적으로 header에 datadog의 정보를 넣어야했지만... )
context가 정확히 무엇인지를 이해해야겠다 싶어 .. 작성하는 글입니다 ..

** go의 기본 패키지. API와 프로세스 간의 deadline(마감일), 취소, 기타 요청 범위 값을 전달하는 방법을 제공하고, 여러 고루틴 또는 서비스가 조정 및 통신해야 하는 동시 또는 분산 시스템에서 실행 흐름 및 데이터 공유를 관리하는 유용.
내가 이해하고 사용한 바로는 API가 호출되었을 때는 어떤 요청 정보가 들어왔는지, 어떤 환경 정보를 가지고 있는지, 요청 API가 시작되고 끝나는 순간까지 해당 요청에 대한 흐름을 한 컨텍스트로 관리할 수 있었고... context의 request context에 데이터를 set/get 하면서 사용할 때 매우 유용했음.
찾아보니 고루틴으로 작업할 때 일정 시간 동안만 작업을 지시하거나 외부에서 작업을 취소할 때 Context랑 같이 사용한다고 한다. (보통 동시에 처리해야 하는 작업을 고루틴으로 실행하는데, 고루틴을 사용할 때 주의할 점이 실행한 고루틴이 일정 시간 안에 반드시 종료될 것이라는게 보장되어야 한다. 이때 Context의 Cancelation 기능을 사용하면 고루틴의 생명주기를 관리할 수 있다.)
func Background() Context
Background는 nil이 아닌 빈 Context를 반환한다. 취소되지 않으며, 값도 없고, 기한도 없다.
일반적으로 기본 기능, 초기화 및 테스트에서 사용되며 들어오는 요청에 대한 최상위 컨텍스트로 사용된다.
func TODO() Context
TODO는 nil이 아닌 빈 컨텍스트를 반환한다. 사용할 Context가 확실하지 않거나 아직 사용할 수 없는 경우(주변 함수가 아직 Context 매개 변수를 허용하도록 확장되지 않았기 때문에) 코드에서는 context.TODO를 사용해야 한다.
func WithValue(parent Context, key, val any) Context
Context가 생성되면 우선 변경할 수 없기 때문에 Value를 넣어야 한다면 해당 func를 사용해 새로운 Context를 만들어주는 개념으로 사용해야한다.
** 문서에는 request-scoped data 를 위해서만 Context Value를 사용하라고 되어있다.
type Context interface {
Value(key interface{}) interface{}
}
package main
import (
"context"
"fmt"
)
func main() {
type favContextKey string
f := func(ctx context.Context, k favContextKey) {
if v := ctx.Value(k); v != nil { // 값이 있을 경우
fmt.Println("found value:", v)
return
}
fmt.Println("key not found:", k) // 값이 없을 경우
}
k := favContextKey("language")
ctx := context.WithValue(context.Background(), k, "Go")
f(ctx, k)
f(ctx, favContextKey("color"))
}
Output:
found value: Go
key not found: color
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
일정 시간이 되면 자동으로 Context에 취소되도록 설정할 수 있는데 withDeadline과 withTimeout 이 있다.
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
withDeadline은 time.Time을 파라미터로 받고, 해당 시간이 되면 Context가 취소된다. 이 때, Deadline이라는 Func을 사용하면 취소가 되기 전까지 남은 시간을 확인할 수 있다.
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
withTimeout은 time.Duration을 파라미터로 받고 해당 시간이 지나면 Context가 취소된다. 무한정으로 고투린 작업 시간이 길어지는 것을 막을 수 있다.
DB insert, update 등 디비 작업시 완전성을 보장하기 위해 DB Transaction을 하는 케이스가 있다. 이 때, db.Begin() 를 사용하게 되는데 db.Begin() 대신 BeginTx(context.Context, opts) 를 사용한다면?
현재 진행하는 프로젝트에서 여러 쿼리를 하나의 트랜잭션으로 묶어서 처리해야 하는 경우가 종종 있는데 이 때 db.beginTx() 를 사용했다. Context를 다시 공부하는 차원에서 소스를 들여다봤는데
ctx := context.Background()
tx, _ := dbConn.BeginTx(ctx, nil)
tx.ExecContext(ctx, [qeury] ...)
특정 로직에서 다음과 같은 방식으로 사용하고 있었다. 하지만, 이 방식은 사실 db.Begin()과 차이가 없다. 왜냐 db.begin() 메서드 호출 시 내부에서 이미 context.Background() 를 생성하고 있기 때문이다.
func (db *DB) Begin() (*Tx, error) {
return db.BeginTx(context.Background(), nil)
}
그럼 context를 활용해서 tracsaction 관리를 어떻게 사용하면 될까?
(이런 케이스를 현재 사용할 지는 모르겠지만...) DB 처리를 동시에 병렬적으로 처리해야하는 경우가 있다면? 근데 혹시나 병렬로 했음에도 불구하고 DB 병목 현상이 생겨 무한 대기해야 하는 상황을 피하고 싶다면?
package main
import (
"context"
"database/sql"
"fmt"
"github.com/sirupsen/logrus"
"log"
"sync"
"time"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "root:root@tcp(localhost:3306)/fearisthemindkiller")
if err != nil {
logrus.Fatal(err)
}
defer db.Close()
// 예제를 위한 테이블 생성
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS test (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50))`)
if err != nil {
log.Fatal(err)
}
// 고루틴을 사용하여 트랜잭션 실행
var wg sync.WaitGroup
wg.Add(2)
// 고루틴 1
go func() {
defer wg.Done()
// Context 생성 및 타임아웃 설정
ctx1, cancel1 := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel1()
// 트랜잭션 시작
tx1, err := db.BeginTx(ctx1, nil)
if err != nil {
logrus.Error("고루틴 1: 트랜잭션 실패")
logrus.Error(err)
return
}
// INSERT 쿼리 실행
_, err = tx1.ExecContext(ctx1, "INSERT INTO test (name) VALUES (?)", "aespa")
if err != nil {
// 롤백
tx1.Rollback()
logrus.Error("고루틴 1: 롤백")
logrus.Error(err)
return
}
// 커밋
err = tx1.Commit()
if err != nil {
logrus.Error("고루틴 1: 커밋 실패")
logrus.Error(err)
return
}
fmt.Println("고루틴 1: 트랜잭션 완료")
}()
// 고루틴 2
go func() {
defer wg.Done()
// Context 생성 및 타임아웃 설정
ctx2, cancel2 := context.WithTimeout(context.Background(), 1*time.Microsecond)
//ctx2, cancel2 := context.WithDeadline(context.Background(), time.Now().Add(1*time.Minute))
defer cancel2()
// 트랜잭션 시작
tx2, err := db.BeginTx(ctx2, nil)
if err != nil {
logrus.Error("고루틴 2: 트랜잭션 실패")
logrus.Error(err)
return
}
// INSERT 쿼리 실행
_, err = tx2.ExecContext(ctx2, "INSERT INTO test (name) VALUES (?)", "newjeans")
if err != nil {
// 롤백
logrus.Error("고루틴 2: 롤백")
tx2.Rollback()
logrus.Error(err)
return
}
// 커밋
err = tx2.Commit()
if err != nil {
logrus.Error("고루틴 1: 커밋 실패")
logrus.Error(err)
return
}
fmt.Println("고루틴 2: 트랜잭션 완료")
}()
// 고루틴이 모두 완료될 때까지 대기
wg.Wait()
// 테이블 내용 출력
rows, err := db.Query("SELECT id, name FROM test")
if err != nil {
logrus.Fatal(err)
}
defer rows.Close()
fmt.Println(">>>>> 결과 확인")
for rows.Next() {
var id int
var name string
if err := rows.Scan(&id, &name); err != nil {
logrus.Fatal(err)
}
fmt.Printf("ID: %d, Name: %s\n", id, name)
}
}

고루틴은 총 2개로, 첫번째 고루틴에서는 타임아웃이 5초인 context와 함께 insert 문을 처리하는 트랜잭션을 실행하였고 두번째 고루틴에서는 동일한 쿼리이지만 극단적으로... 1 마이크로초의 타임아웃을 가진 Context와 함께 insert 문을 실행.
결과적으로
타임아웃이 5초인 Context를 사용한 트랜잭션은 성공적으로 insert 했으나 (aespa), 타임아웃이 1 마이크로초인 Conext를 사용한 트랜잭션은 context deadline exceed 라는 에러 문구를 내뱉으며 실패했다 ...
그럼 타임아웃을 모두 5초로 변경한 후 다시 실행하면?

아주 정상적으로 추가 완료 ... ~
고루틴없이 그냥 db tracsaction 의 context를 활용해서 트랜잭션을 보장하고 싶다면?
테스트 순서는 다음과 같다.
1. kpop group과 kpop member 데이터를 테이블에 insert한다. (이 때, 트랜잭션으로 묶음)
2. 정상적인 케이스 - group / member 테이블에 모두 잘 insert됨
3. 에러 케이스 - group insert 시 정상적으로 작동하나, member insert 시 일부러 error 발생했다고 가정
3-1. 트랜잭션 rollback 처리로 정상 insert됐던 group 데이터도 rollback
package main
import (
"context"
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/sirupsen/logrus"
"log"
"time"
)
func main() {
db, err := sql.Open("mysql", "root:root@tcp(localhost:3306)/fearisthemindkiller")
if err != nil {
logrus.Fatal(err)
}
defer db.Close()
// 예제를 위한 테이블 생성
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS kpop_group (group_id INT AUTO_INCREMENT PRIMARY KEY, group_name VARCHAR(50))`)
if err != nil {
log.Fatal(err)
}
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS kpop_member (member_id INT AUTO_INCREMENT PRIMARY KEY, group_id INT, member_name VARCHAR(50))`)
if err != nil {
log.Fatal(err)
}
context, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
// 트랜잭션 시작
tx, err := db.BeginTx(context, nil)
if err != nil {
logrus.Error("트랜잭션 시작")
logrus.Error(err)
return
}
// INSERT 쿼리 실행
result, err := tx.ExecContext(context, "INSERT INTO kpop_group (group_name) VALUES (?)", "aspea")
if err != nil {
logrus.Error(err)
tx.Rollback()
return
}
id, _ := result.LastInsertId()
// INSERT 쿼리 실행
_, err = tx.ExecContext(context, "INSERT INTO kpop_member (group_id, member_name) VALUES (?, ?)", id, "karina")
if err != nil {
logrus.Error(err)
tx.Rollback()
return
}
err = tx.Commit()
if err != nil {
logrus.Error(err)
return
}
// 그룹 테이블 내용 출력
groupRows, err := db.Query("SELECT group_id, group_name FROM kpop_group")
if err != nil {
logrus.Fatal(err)
defer groupRows.Close()
}
// 멤버 테이블 내용 출력
memberRows, err := db.Query("SELECT member_id, group_id, member_name FROM kpop_member")
if err != nil {
logrus.Error("????")
logrus.Fatal(err)
}
defer memberRows.Close()
fmt.Println(">>>>> 결과 확인")
for groupRows.Next() {
var groupId int
var groupName string
if err := groupRows.Scan(&groupId, &groupName); err != nil {
logrus.Fatal(err)
}
fmt.Printf("ID: %d, Name: %s\n", groupId, groupName)
}
for memberRows.Next() {
var memberId int
var groupId int
var memberName string
if err := memberRows.Scan(&memberId, &groupId, &memberName); err != nil {
logrus.Fatal(err)
}
fmt.Printf("ID: %d, GROUP : %d, Name: %s\n", memberId, groupId, memberName)
}
}

// INSERT 쿼리 실행
result, err := tx.ExecContext(context, "INSERT INTO kpop_group (group_name) VALUES (?)", "newjeans")
if err != nil {
logrus.Error(err)
tx.Rollback()
return
}
id, _ := result.LastInsertId()
logrus.Info("group insert OK")
// INSERT 쿼리 실행
_, err = tx.ExecContext(context, "INSERT INTO kpop_member (group_id, member_name) VALUES (?, ?)", id, "minji")
if err == nil { // 에러가 발생했다고 가정
logrus.Error(err)
logrus.Error("member 에러 발생 - rollback")
tx.Rollback() // 롤백 처리
return
}



참고