Go의 slog에서 민감한 데이터 가리기: masq와 함께하는 실용 가이드

발행: (2026년 1월 18일 오전 09:24 GMT+9)
10 min read
원문: Dev.to

I’m happy to translate the article for you, but I need the actual text you’d like translated. Could you please paste the content (or the portion you want translated) here? Once I have the text, I’ll provide a Korean translation while preserving the original formatting, markdown syntax, and any code blocks or URLs.

문제: 로그에서 민감한 데이터 유출

일반적인 상황을 생각해 보세요: 디버깅을 위해 User 구조체를 로그에 남기고 있습니다.

type User struct {
    ID       string
    Email    string
    APIToken string
}

func main() {
    user := User{
        ID:       "u123",
        Email:    "alice@example.com",
        APIToken: "sk-secret-token-12345",
    }
    slog.Info("user logged in", "user", user)
}

출력

level=INFO msg="user logged in" user="{ID:u123 Email:alice@example.com APIToken:sk-secret-token-12345}"

앗. 이제 API 토큰이 로그에 남아 로그 접근 권한이 있는 누구든지 볼 수 있게 되었으며, 수개월·수년 동안 보관될 수 있고 삭제하기가 거의 불가능합니다.

slog의 내장 솔루션: LogValuer

Go의 slog 패키지(Go 1.21부터 표준 라이브러리)에서는 값이 로그에 어떻게 표시될지를 커스터마이징할 수 있도록 LogValuer 인터페이스를 제공합니다:

type APIToken string

func (APIToken) LogValue() slog.Value {
    return slog.StringValue("[REDACTED]")
}

func main() {
    token := APIToken("sk-secret-token-12345")
    slog.Info("token received", "token", token)
}

출력

level=INFO msg="token received" token=[REDACTED]

이는 직접 값에 대해서는 정상적으로 동작합니다. 하지만 LogValuer는 구조체 필드에 대해서는 동작하지 않습니다:

type APIToken string

func (APIToken) LogValue() slog.Value {
    return slog.StringValue("[REDACTED]")
}

type Credentials struct {
    UserID string
    Token  APIToken
}

func main() {
    creds := Credentials{
        UserID: "u123",
        Token:  "sk-secret-token-12345",
    }
    slog.Info("credentials", "creds", creds)
}

출력 (토큰이 노출됨!)

level=INFO msg=credentials creds="{UserID:u123 Token:sk-secret-token-12345}"

구조체를 로그에 기록할 때 slog는 리플렉션을 사용하고, 중첩된 필드에 정의된 LogValue() 메서드를 우회합니다. 이 제한 때문에 실제 애플리케이션에서 민감한 데이터를 보호하기가 어렵습니다.

masq: 자동 깊은 마스킹

masqslogReplaceAttr 옵션에 연결되어 중첩 구조체를 포함한 모든 로그 값들을 재귀적으로 검사하여 민감한 데이터를 마스킹합니다.

기본 사용법

package main

import (
    "log/slog"
    "os"

    "github.com/m-mizutani/masq"
)

type EmailAddr string

type User struct {
    ID    string
    Email EmailAddr
}

func main() {
    // Create a logger with masq redaction
    logger := slog.New(
        slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
            ReplaceAttr: masq.New(masq.WithType[EmailAddr]()),
        }),
    )

    user := User{
        ID:    "u123",
        Email: "alice@example.com",
    }

    logger.Info("user registered", "user", user)
}

예시 출력

{
  "time": "2026-01-18T12:00:00.000Z",
  "level": "INFO",
  "msg": "user registered",
  "user": {
    "ID": "u123",
    "Email": "[FILTERED]"
  }
}

이메일 주소는 중첩 구조체에 포함되어 있더라도 자동으로 마스킹됩니다.

Source:

마스킹 전략

masq는 민감한 데이터를 식별하고 마스킹하는 여러 방법을 제공합니다.

1. 사용자 정의 타입별

민감한 데이터를 별개의 타입으로 정의하고 마스킹합니다:

type Password string
type CreditCard string

masq.New(
    masq.WithType[Password](),
    masq.WithType[CreditCard](),
)

2. 구조체 태그별

구조체 태그를 사용해 민감한 필드를 표시합니다:

type User struct {
    ID       string
    Password string `masq:"secret"`
    SSN      string `masq:"secret"`
}

masq.New(masq.WithTag("secret"))

3. 필드 이름별

특정 필드 이름을 대상으로 합니다:

masq.New(
    masq.WithFieldName("Password"),
    masq.WithFieldName("APIKey"),
)

4. 필드 접두사별

주어진 접두사로 시작하는 모든 필드를 마스킹합니다:

type Config struct {
    SecretKey      string // redacted
    SecretToken    string // redacted
    PublicEndpoint string // not redacted
}

masq.New(masq.WithFieldPrefix("Secret"))

5. 정규식 패턴별

정규식에 맞는 값을 찾아 마스킹합니다(신용카드 번호, 전화번호 등에 유용합니다):

import "regexp"

// 잠재적인 신용카드 번호(16자리)를 마스킹
cardPattern := regexp.MustCompile(`\b\d{16}\b`)

masq.New(masq.WithRegex(cardPattern))

6. 문자열 내용별

특정 문자열을 포함하는 모든 값을 마스킹합니다:

masq.New(masq.WithContain("Bearer "))

여러 전략 결합

You can combine any number of redaction rules:

logger := slog.New(
    slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        ReplaceAttr: masq.New(
            // Redact by type
            masq.WithType[Password](),
            masq.WithType[APIToken](),

            // Redact by tag
            masq.WithTag("secret"),

            // Redact by field name
            masq.WithFieldName("SSN"),

            // Redact by prefix
            masq.WithFieldPrefix("Secret"),

            // Redact by regex
            masq.WithRegex(regexp.MustCompile(`\b\d{16}\b`)),

            // Redact by string content
            masq.WithContain("Bearer "),
        ),
    }),
)

이제 모든 로그 항목이 구성된 모든 필터를 통과하므로, 얼마나 깊게 중첩되었든 민감한 정보가 누출되지 않도록 보장합니다.

TL;DR

  • Problem: slogLogValuer는 중첩된 필드를 마스킹할 수 없습니다.
  • Solution: masq를 사용하여 로그 값들을 재귀적으로 검사하고 필터링합니다.
  • How: masq.New(...)slog.HandlerOptions.ReplaceAttr에 연결하고, 하나 이상의 식별 전략(타입, 태그, 필드 이름, 접두사, 정규식, 문자열 내용)을 선택합니다.

한번 시도해 보세요, 그리고 로그를 깔끔하고 규정에 맞게 유지하세요!

실제 예시

다음은 일반 웹 애플리케이션 컨텍스트에서 masq를 보여주는 완전한 예시입니다:

package main

import (
    "log/slog"
    "os"
    "regexp"

    "github.com/m-mizutani/masq"
)

type (
    Password  string
    AuthToken string
)

type LoginRequest struct {
    Username string
    Password Password
}

type UserSession struct {
    UserID      string `masq:"pii"`
    AuthToken   AuthToken
    Email       string `masq:"pii"`
    PhoneNumber string
}

func main() {
    // Configure comprehensive redaction
    logger := slog.New(
        slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
            ReplaceAttr: masq.New(
                masq.WithType[Password](),
                masq.WithType[AuthToken](),
                masq.WithTag("pii"),
                masq.WithRegex(regexp.MustCompile(`^\+[1-9]\d{10,14}$`)), // phone numbers
            ),
        }),
    )
    slog.SetDefault(logger)

    // Simulate login flow
    req := LoginRequest{
        Username: "alice",
        Password: "super-secret-password",
    }
    slog.Info("login attempt", "request", req)

    session := UserSession{
        UserID:      "u123",
        AuthToken:   "tok_abc123xyz",
        Email:       "alice@example.com",
        PhoneNumber: "+14155551234",
    }
    slog.Info("session created", "session", session)
}

출력

{"time":"2026-01-18T12:00:00.000Z","level":"INFO","msg":"login attempt","request":{"Username":"alice","Password":"[FILTERED]"}}
{"time":"2026-01-18T12:00:00.000Z","level":"INFO","msg":"session created","session":{"UserID":"u123","AuthToken":"[FILTERED]","Email":"[FILTERED]","PhoneNumber":"[FILTERED]"}}

모든 민감한 데이터는 자동으로 마스킹되며 구조와 비민감 필드는 그대로 유지됩니다.

모범 사례

  • 민감한 데이터에 대한 사용자 정의 타입 정의 – 비밀번호나 토큰에 일반 string을 사용하는 대신, 별도의 타입(e.g., type Password string)을 만들세요. 이렇게 하면 마스킹이 명시적이며 컴파일 시점에 문제를 잡을 수 있습니다.
  • 외부 데이터에 struct 태그 사용 – 서드파티 struct나 데이터베이스 모델을 사용할 때, 민감한 필드를 표시하기 위해 masq:"secret" 태그를 추가하세요.
  • 정규식 패턴을 신중히 적용 – 정규식 매칭은 모든 문자열 값에 대해 실행되므로, 성능 문제를 피하기 위해 구체적인 패턴을 사용하세요.
  • 마스킹을 테스트 – 민감한 데이터가 로그 출력에 나타나지 않음을 검증하는 테스트를 작성하세요.
  • 기본적으로 마스킹 적용 – 확신이 서지 않을 때는 마스킹하세요. 마스킹을 제거하는 것이 유출된 데이터를 정리하는 것보다 쉽습니다.

제한 사항

  • 프라이빗 맵 필드masq는 내장된 프라이빗 맵 타입을 신뢰성 있게 복제하지 못하며, nil이 됩니다.
    민감한 데이터는 구조체 필드를 사용하세요.

  • 성능 – 깊은 검사는 약간의 오버헤드를 추가합니다.
    고처리량 시스템의 경우 샘플링이나 비동기 로깅을 고려하세요.

결론

로그에서 민감한 데이터 유출을 방지하는 것은 보안 및 규정 준수에 필수적입니다. slogLogValuer가 직접 값에 도움이 되는 반면, masq는 중첩 구조체까지 보호를 확장하고 유연한 마스킹 전략을 제공합니다.

masq 를 사용해 보고 의견을 알려 주세요!

이 내용이 도움이 되었다면, GitHub에서 레포지토리에 ⭐를 눌러 주시거나 아래 댓글에 피드백을 공유해 주세요.

Back to Blog

관련 글

더 보기 »

Gin vs Spring Boot: 자세한 비교

Gin vs Spring Boot: 자세한 비교용 커버 이미지 https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%...