
Go 1.21 버전부터 추가된 slog 는 구조화된 로깅을 지원하며,
특히 Handler 기반 구조를 통해 로깅 로직을 유연하게 확장 할 수 있는데요.
실무에서는 로그 추적을 위해서 다음과 같은 메타데이터를 남기는 경우가 많이 있습니다.
- request_id
- trace_id
- user_id
- service
- ip
- path
위 값들은 고유 값들로 각 요청과 종료의 범위, 흐름을 파악할 때 유용하게 사용이 됩니다.
이 값을 매번 수동으로 넣는 방식을 사용하면 번거럽고, 누락하는 문제가 발생하기도 합니다.
logger.Info("payment completed",
slog.String("request_id", requestID),
slog.String("user_id", userID),
)
위 로그만 본다면 단순하고 어려울 것도 없지만, 서비의 규모가 커질 수록 동일한 코드가 이곳 저곳 반복 작성이 될 것입니다.
1. Context Handler 패턴이란?
Request -> Context Save -> slog Handler 추출 -> add log
Context Handler 패턴은 어플리케이션의 실행 흐름을 저장하고 있는 context.Context와 로깅 시스템을 연결하는 역할을 합니다.
일반적으로 요청이 들어오면 미들웨어에서 request_id 등과 같은 고유 키를 만들어 context에 저장합니다.
이를 로그로 남기려면 매번 로깅 함수에 인자로 넘겨주어야하는데, context Handler는 로그가 출력되기 직전에 context 내부에 필요한 메타데이터 로그를 합쳐서 만들어 줍니다.
2. 이 방법을 쓰는 이유
- 매 호출마다 반복적인 request_id 같은 동일 인자값을 로깅 함수에 인자 값으로 넘겨주지 않아도 됩니다.
- 실수로 ID 값들을 누락하더라도 핸들러 차원에서 주입해서 누락 방지를 해줍니다
- 비즈니스 로직에서는 비즈니스 함수가 로깅을 위한 파라미터를 들고 다니지 않아도 됩니다. (관심사 분리)
3. 예제 코드
package main
import (
"context"
"log/slog"
"os"
)
type ctxKey string
const (
RequestIDKey ctxKey = "request_id"
TraceIDKey ctxKey = "trace_id"
)
type ContextHandler struct {
slog.Handler
}
func (h *ContextHandler) Handle(ctx context.Context, r slog.Record) error {
if rid, ok := ctx.Value(RequestIDKey).(string); ok {
r.AddAttrs(slog.String("request_id", rid))
}
if tid, ok := ctx.Value(TraceIDKey).(string); ok {
r.AddAttrs(slog.String("trace_id", tid))
}
return h.Handler.Handle(ctx, r)
}
func (h *ContextHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return &ContextHandler{Handler: h.Handler.WithAttrs(attrs)}
}
func (h *ContextHandler) WithGroup(name string) slog.Handler {
return &ContextHandler{Handler: h.Handler.WithGroup(name)}
}
func AppendCtx(parent context.Context, rid, tid string) context.Context {
ctx := context.WithValue(parent, RequestIDKey, rid)
return context.WithValue(ctx, TraceIDKey, tid)
}
func main() {
innerHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
})
logger := slog.New(&ContextHandler{innerHandler})
// 요청 컨텍스트 생성
ctx := AppendCtx(context.Background(), "REQ-12345", "TRACE-67890")
logger.InfoContext(ctx, "user login attempt", slog.String("user_email", "reo@example.com"))
performBusinessLogic(ctx, logger)
}
func performBusinessLogic(ctx context.Context, log *slog.Logger) {
// 비즈니스 로직 함수는 내부에서 request_id나 trace_id를 전혀 몰라도 된다.
log.DebugContext(ctx, "database query completed")
}
'프로그래밍 > golang' 카테고리의 다른 글
| [Golang] io.Pipe (0) | 2026.03.28 |
|---|---|
| [Golang] io 패키지 기초 (0) | 2026.03.27 |
| [Golang] net/url 패키지를 활용한 SSRF 방어 (0) | 2026.03.26 |
| [Golang] 자식 프로세스 생명 주기 관리 (Shutdown) (0) | 2026.03.26 |
| [Golang] net/url 기초 (0) | 2026.03.24 |