gin.Context 설명: 단순한 Context 그 이상
Grace Collins
Solutions Engineer · Leapcell

먼저 gin.Context(또는 echo.Context)의 설계 목적을 이해해야 합니다. 이는 웹 프레임워크에 특화된 컨텍스트 객체로, 단일 HTTP 요청을 처리하는 데 사용됩니다. 그 책임 범위는 매우 넓습니다.
- 요청 파싱: 경로 파라미터(
c.Param()
), 쿼리 파라미터(c.Query()
), 요청 헤더(c.Header()
), 요청 본문(c.BindJSON()
)을 얻습니다. - 응답 작성: JSON(
c.JSON()
), HTML(c.HTML()
)을 반환하고, 상태 코드(c.Status()
)를 설정하고, 응답 헤더를 작성합니다. - 미들웨어 데이터 전달: 미들웨어 체인 간에 데이터를 전달합니다(
c.Set()
,c.Get()
). - 흐름 제어: 미들웨어 체인을 중단합니다(
c.Abort()
).
이러한 모든 기능이 HTTP 프로토콜에 긴밀하게 연결되어 있음을 알 수 있습니다.
그렇다면 context.Context는 어디에서 사용될까요?
핵심: gin.Context
는 내부적으로 표준 context.Context를 포함합니다.
Gin에서는 c.Request.Context()
를 통해 이를 얻을 수 있습니다. 이 내장된 context.Context
는 이전 글에서 논의한 핵심 기능인 취소, 타임아웃 및 메타데이터 전파를 모두 수행합니다.
func MyGinHandler(c *gin.Context) { // gin.Context에서 표준 context.Context 가져오기 ctx := c.Request.Context() // 이제 이 ctx를 사용하여 표준 context가 수행해야 하는 모든 작업을 수행할 수 있습니다. // ... }
왜 이러한 분리가 필요할까요? 계층화 및 분리
이는 훌륭한 소프트웨어 디자인의 구현입니다. 즉, 관심사 분리입니다.
- HTTP 계층(컨트롤러/핸들러): HTTP 세계와 상호 작용하는 것이 책임입니다.
gin.Context
를 사용하여 요청을 파싱하고 응답 형식을 지정해야 합니다. - 비즈니스 로직 계층(서비스): 핵심 비즈니스 로직(계산, 데이터베이스 작업, 다른 서비스 호출)을 실행하는 것이 책임입니다. HTTP 또는 JSON이 무엇인지 알 필요가 없습니다. 작업 수명 주기(취소 여부)와 실행에 필요한 메타데이터(예: TraceID)만 신경 쓰면 됩니다. 따라서 비즈니스 로직 계층의 모든 함수는
context.Context
만 허용해야 합니다.
UserService
가 gin.Context
에 의존하면 어떻게 될까요?
// 나쁜 디자인: 긴밀하게 결합됨 type UserService struct { ... } func (s *UserService) GetUserDetails(c *gin.Context, userID string) (*User, error) { // ... }
이 디자인에는 몇 가지 심각한 결함이 있습니다.
- 재사용 불가능: 어느 날 gRPC 서비스, 백그라운드 작업 또는 메시지 큐 소비자에서
GetUserDetails
를 호출해야 하는 경우 어떻게 하시겠습니까? 전달할*gin.Context
가 없으며 이 함수를 재사용할 수 없습니다. - 테스트하기 어려움:
GetUserDetails
를 테스트하려면*gin.Context
객체를 꼼꼼하게 모의해야 하며, 이는 번거롭고 직관적이지 않습니다. - 불명확한 책임:
UserService
는 이제 HTTP 계층의 세부 사항을 알게 되어 단일 책임 원칙을 위반합니다.
모범 사례: 명확한 경계 및 "핸드오버"
올바른 접근 방식은 HTTP 핸들러 계층에서 gin.Context
에서 context.Context
로의 "핸드오버"를 완료하는 것입니다.
핸들러를 외부 세계(HTTP 요청)의 언어를 내부 세계(비즈니스 로직)의 언어로 번역하는 어댑터로 생각하세요.
다음은 모범 사례를 따르는 전체 프로세스입니다.
1. 순수 비즈니스 로직 계층(서비스 계층) 정의
함수 서명은 context.Context
만 허용하고 Gin의 존재를 인식하지 못합니다.
// service/user_service.go package service import "context" type UserService struct { // 종속성, 예: 데이터베이스 연결 풀 } func (s *UserService) GetUser(ctx context.Context, userID string) (*User, error) { // 컨텍스트를 통해 전달된 TraceID를 출력합니다. if traceID, ok := ctx.Value("traceID").(string); ok { log.Printf("Service layer processing GetUser for %s with TraceID: %s", userID, traceID) } // 시간이 오래 걸리는 데이터베이스 쿼리를 시뮬레이션하고 취소 신호를 수신합니다. select { case <-ctx.Done(): log.Println("Database query canceled:", ctx.Err()) return nil, ctx.Err() // 취소 오류를 위로 전파합니다. case <-time.After(100 * time.Millisecond): // 쿼리 지연을 시뮬레이션합니다. // ... 실제 데이터베이스 쿼리: db.QueryRowContext(ctx, ...) log.Printf("User %s found in database", userID) return &User{ID: userID, Name: "Alice"}, nil } }
2. HTTP 처리 계층(핸들러/컨트롤러 계층) 작성
핸들러의 책임은 다음과 같습니다.
gin.Context
를 사용하여 HTTP 요청 파라미터를 파싱합니다.gin.Context
에서 표준context.Context
를 얻습니다.- 비즈니스 로직 계층에서 해당 메서드를 호출하여
context.Context
와 파싱된 파라미터를 전달합니다. gin.Context
를 사용하여 비즈니스 로직 계층의 결과를 HTTP 응답으로 포맷합니다.
// handler/user_handler.go package handler import ( "net/http" "my-app/service" // 서비스 패키지를 가져옵니다. "github.com/gin-gonic/gin" ) type UserHandler struct { userService *service.UserService } func NewUserHandler(us *service.UserService) *UserHandler { return &UserHandler{userService: us} } func (h *UserHandler) GetUser(c *gin.Context) { // 1. gin.Context를 사용하여 파라미터를 파싱합니다. userID := c.Param("id") // 2. gin.Context에서 표준 context.Context를 얻습니다. ctx := c.Request.Context() // 3. 비즈니스 로직 계층을 호출하여 "핸드오버"를 완료합니다. user, err := h.userService.GetUser(ctx, userID) if err != nil { // 오류가 컨텍스트 취소로 인해 발생했는지 확인합니다. if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { c.JSON(http.StatusRequestTimeout, gin.H{"error": "request canceled or timed out"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // 4. gin.Context를 사용하여 응답을 포맷합니다. c.JSON(http.StatusOK, user) }
3. main.go
(또는 정의된 라우터 계층)에서 모든 것을 조립합니다.
프로그램의 진입점에서 모든 종속성을 초기화하고 필요한 곳에 "주입"합니다.
// main.go package main import ( "my-app/handler" "my-app/service" "github.com/gin-gonic/gin" ) // TraceID를 추가하는 간단한 미들웨어 func TraceMiddleware() gin.HandlerFunc { return func(c *gin.Context) { traceID := uuid.New().String() // context.WithValue를 사용하여 TraceID가 있는 새 컨텍스트를 생성합니다. // 참고: 이것이 표준 컨텍스트를 수정하는 올바른 방법입니다. ctx := context.WithValue(c.Request.Context(), "traceID", traceID) // 원래 요청 컨텍스트를 새 컨텍스트로 바꿉니다. c.Request = c.Request.WithContext(ctx) // 선택적으로 핸들러가 직접 사용할 수 있도록 gin.Context에 복사본을 저장합니다(필요하지 않음). c.Set("traceID", traceID) c.Next() } } func main() { // 비즈니스 로직 계층을 초기화합니다. userService := &service.UserService{} // HTTP 처리 계층을 초기화하고 종속성을 주입합니다. userHandler := handler.NewUserHandler(userService) router := gin.Default() router.Use(TraceMiddleware()) // 추적 미들웨어를 사용합니다. router.GET("/users/:id", userHandler.GetUser) router.Run(":8080") }
요약: 이 패턴을 기억하세요.
HTTP 핸들러(예: Gin)
- 사용되는 컨텍스트 유형:
*gin.Context
- 핵심 책임: HTTP 요청 파싱, 비즈니스 로직 호출, HTTP 응답 포맷.
gin.Context
와context.Context
간의 핸드오버 지점입니다.
비즈니스 로직 계층(서비스)
- 사용되는 컨텍스트 유형:
context.Context
- 핵심 책임: 핵심 비즈니스 로직 실행, 데이터베이스, 캐시 및 기타 마이크로서비스와 상호 작용. 웹 프레임워크와 완전히 분리됩니다.
데이터 액세스 계층(리포지토리)
- 사용되는 컨텍스트 유형:
context.Context
- 핵심 책임:
db.QueryRowContext(ctx, ...)
와 같은 구체적인 데이터베이스/캐시 작업을 수행합니다.
이러한 계층화 및 분리 패턴은 엄청난 유연성을 제공합니다.
- 이식성:
service
패키지를 있는 그대로 가져와 다른 Go 프로그램에서 사용할 수 있습니다. - 테스트 용이성:
UserService
테스트가 매우 간단해집니다. 복잡한 HTTP 환경을 모의할 필요 없이context.Background()
와 문자열 ID만 있으면 됩니다. - 명확한 아키텍처: 각 구성 요소의 책임이 명확하여 코드를 더 쉽게 이해하고 유지 관리할 수 있습니다.
Go 프로젝트 호스팅에 가장 적합한 Leapcell입니다.
Leapcell은 웹 호스팅, 비동기 작업 및 Redis를 위한 차세대 서버리스 플랫폼입니다.
다국어 지원
- Node.js, Python, Go 또는 Rust로 개발하세요.
무제한 프로젝트를 무료로 배포하세요.
- 사용량에 대해서만 지불하세요. 요청도 없고 요금도 없습니다.
타의 추종을 불허하는 비용 효율성
- 유휴 요금 없이 사용량에 따라 지불하세요.
- 예: $25는 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅.
간편한 확장성 및 고성능
- 높은 동시성을 쉽게 처리하기 위한 자동 확장.
- 운영 오버헤드가 전혀 없습니다. 빌드에만 집중하세요.
Documentation에서 더 자세히 알아보세요.
X에서 팔로우하세요: @LeapcellHQ