JSON Formatter Go — MarshalIndent() 가이드
무료 JSON 포맷터을 브라우저에서 직접 사용하세요 — 설치 불필요.
JSON 포맷터 온라인으로 사용하기 →Go 마이크로서비스 작업 중 API 응답이나 설정 파일을 살펴봐야 할 때, 압축된 JSON은 첫 번째 장벽이 됩니다——수백 개의 중첩 필드가 한 줄에 몰려 있으면 한눈에 파악하기가 거의 불가능합니다. Go에서 JSON을 포맷하려면 표준 라이브러리가 필요한 모든 것을 제공합니다: json.MarshalIndent 는 encoding/json 에 내장되어 있고, 모든 Go 설치에 함께 제공되며, 서드파티 의존성이 전혀 없습니다. 이 가이드는 전체 내용을 다룹니다: struct tags, 커스텀 MarshalJSON() 구현, 원시 바이트를 재포맷하는 json.Indent,json.Decoder 로 대용량 파일 스트리밍, 고처리량 경로에서 go-json 을 쓸 시점, 그리고 터미널에서 빠르게 포맷하는 CLI 원라이너까지. 모든 예제는 Go 1.21+ 기준입니다.
- ✓json.MarshalIndent(v, "", "\t")는 표준 라이브러리——제로 의존성, 모든 Go 설치에 포함됨.
- ✓struct tags json:"field_name,omitempty"로 직렬화 키를 제어하고 zero value 필드를 출력에서 생략할 수 있음.
- ✓임의 타입에 MarshalJSON()을 구현하면 해당 타입의 JSON 표현을 완전히 제어할 수 있음.
- ✓json.Indent()는 struct 재파싱 없이 이미 마샬링된 []byte를 재포맷——원시 바이트에 더 빠름.
- ✓대용량 파일(>100 MB)의 경우: Token()을 사용한 json.Decoder로 전체를 메모리에 로드하지 않고 스트리밍.
- ✓go-json은 encoding/json의 드롭인 대체품으로 고처리량 API에서 3~5배 빠름.
JSON 포맷팅이란?
JSON 포맷팅(pretty-printing이라고도 함)은 압축된 JSON 문자열을 일관된 들여쓰기와 줄바꿈이 있는 사람이 읽기 쉬운 레이아웃으로 변환합니다. 기저 데이터는 동일하며, 변하는 것은 공백뿐입니다. 압축 JSON은 바이트 수가 중요한 네트워크 전송에 최적이고, 포맷된 JSON은 디버깅, 코드 리뷰, 로그 검사, 설정 파일 작성에 최적입니다. Go의 encoding/json 패키지는 단일 함수 호출로 두 가지 모드를 모두 처리합니다—— json.Marshal 과 json.MarshalIndent 사이를 전환하는 것만으로 압축과 들여쓰기 출력을 자유롭게 선택할 수 있습니다.
{"service":"payments","port":8443,"workers":4}{
"service": "payments",
"port": 8443,
"workers": 4
}json.MarshalIndent() — 표준 라이브러리 방식
json.MarshalIndent 는 Go 표준 라이브러리의 일부인 encoding/json 패키지에 있으며, go get 이 필요 없습니다. 함수 시그니처는 MarshalIndent(v any, prefix, indent string) ([]byte, error):prefix 문자열은 모든 출력 줄의 앞에 추가되고(거의 항상 비워둠), indent 는 중첩 수준마다 한 번씩 반복됩니다. 탭에는 "\t"를, 2칸 공백에는 " " 를 전달하세요.
package main
import (
"encoding/json"
"fmt"
"log"
)
type ServiceConfig struct {
Host string `json:"host"`
Port int `json:"port"`
Workers int `json:"workers"`
TLSEnabled bool `json:"tls_enabled"`
AllowedIPs []string `json:"allowed_ips"`
}
func main() {
cfg := ServiceConfig{
Host: "payments-api.internal",
Port: 8443,
Workers: 8,
TLSEnabled: true,
AllowedIPs: []string{"10.0.0.0/8", "172.16.0.0/12"},
}
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
log.Fatalf("marshal config: %v", err)
}
fmt.Println(string(data))
}
// {
// "host": "payments-api.internal",
// "port": 8443,
// "workers": 8,
// "tls_enabled": true,
// "allowed_ips": [
// "10.0.0.0/8",
// "172.16.0.0/12"
// ]
// }탭과 공백 중 무엇을 선택할지는 주로 팀의 관례에 달려 있습니다. gofmt (Go 소스 코드를 포맷하는 도구)가 탭을 사용하기 때문에 많은 Go 프로젝트가 탭을 선호합니다. JSON이 JavaScript나 Python 소비자를 위한 경우에는 2칸이나 4칸 공백 들여쓰기가 일반적입니다. 같은 struct를 두 가지 들여쓰기 스타일로 나란히 비교한 예를 보여드립니다:
// 탭 들여쓰기——Go 네이티브 툴링에서 선호
data, _ := json.MarshalIndent(cfg, "", " ")
// {
// "host": "payments-api.internal",
// "port": 8443
// }
// 2칸 공백 들여쓰기——JS/Python 소비자 API에서 일반적
data, _ = json.MarshalIndent(cfg, "", " ")
// {
// "host": "payments-api.internal",
// "port": 8443
// }
// 4칸 공백 들여쓰기
data, _ = json.MarshalIndent(cfg, "", " ")
// {
// "host": "payments-api.internal",
// "port": 8443
// }json.Marshal(v) 를 사용하세요. 동일한 값 인자를 받고 동일한 에러 시맨틱을 가지지만, 공백 없이 한 줄 JSON을 생성합니다.Struct Tags — 필드명과 Omitempty 제어
Go struct tags는 필드 선언 뒤에 오는 문자열 리터럴로,encoding/json에 각 필드를 어떻게 직렬화할지 알려줍니다. 세 가지 주요 디렉티브가 있습니다: json:"name" 은 출력에서 필드를 리네임하고, omitempty 는 필드가 해당 타입의 zero value를 가질 때 생략하며, json:"-" 는 필드를 완전히 제외합니다——비밀번호, 내부 식별자, 서비스 경계를 절대 벗어나선 안 되는 필드에 유용합니다.
type UserProfile struct {
ID string `json:"id"`
Email string `json:"email"`
DisplayName string `json:"display_name,omitempty"` // 빈 문자열이면 생략
AvatarURL *string `json:"avatar_url,omitempty"` // nil 포인터면 생략
IsAdmin bool `json:"is_admin,omitempty"` // false면 생략
passwordHash string // 익스포트 안됨——자동 제외
}
// 모든 선택적 필드가 채워진 사용자
full := UserProfile{
ID: "usr_7b3c", Email: "ops@example.com",
DisplayName: "김민준", IsAdmin: true,
}
// {
// "id": "usr_7b3c",
// "email": "ops@example.com",
// "display_name": "김민준",
// "is_admin": true
// }
// 선택적 필드가 없는 사용자——완전히 생략됨
minimal := UserProfile{ID: "usr_2a91", Email: "dev@example.com"}
// {
// "id": "usr_2a91",
// "email": "dev@example.com"
// }json:"-" 태그는 값에 관계없이 무조건적으로 제외해야 하는 필드에 적합합니다——보통 시크릿, 내부 추적 필드, 또는 메모리에서는 올바르지만 외부 시스템에 직렬화되어선 안 되는 데이터입니다.
type AuthToken struct {
TokenID string `json:"token_id"`
Subject string `json:"sub"`
IssuedAt int64 `json:"iat"`
ExpiresAt int64 `json:"exp"`
SigningKey []byte `json:"-"` // 절대 직렬화되지 않음
RefreshToken string `json:"-"` // 절대 직렬화되지 않음
}
tok := AuthToken{
TokenID: "tok_8f2a", Subject: "usr_7b3c",
IssuedAt: 1741614120, ExpiresAt: 1741617720,
SigningKey: []byte("secret"), RefreshToken: "rt_9e4f",
}
data, _ := json.MarshalIndent(tok, "", " ")
// {
// "token_id": "tok_8f2a",
// "sub": "usr_7b3c",
// "iat": 1741614120,
// "exp": 1741617720
// }
// SigningKey와 RefreshToken은 절대 나타나지 않음encoding/json 에 의해 제외됩니다. 익스포트되지 않은 필드에 json:"-" 를 추가할 필요가 없습니다——제외는 자동이며 재정의할 수 없습니다.커스텀 MarshalJSON() — 비표준 타입 처리
모든 Go 타입은 MarshalJSON() ([]byte, error) 메서드를 정의하여 json.Marshaler 인터페이스를 구현할 수 있습니다. encoding/json 이 그러한 타입을 만나면 기본 리플렉션 기반 마샬링 대신 해당 메서드를 호출합니다. 이것은 특정 wire 표현이 필요한 도메인 타입——통화 값, 상태 열거형, 커스텀 시간 형식, 저장 방식과 직렬화 방식이 다른 타입——에 대한 Go의 표준 패턴입니다.
커스텀 타입——Money(원 단위 변환)
package main
import (
"encoding/json"
"fmt"
"log"
)
type Money struct {
Amount int64 // 부동소수점 오차를 피하기 위해 전 단위로 저장
Currency string
}
func (m Money) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Amount float64 `json:"amount"`
Currency string `json:"currency"`
Display string `json:"display"`
}{
Amount: float64(m.Amount) / 100,
Currency: m.Currency,
Display: fmt.Sprintf("%s %.2f", m.Currency, float64(m.Amount)/100),
})
}
type Invoice struct {
ID string `json:"id"`
Subtotal Money `json:"subtotal"`
Tax Money `json:"tax"`
Total Money `json:"total"`
}
func main() {
inv := Invoice{
ID: "inv_9a2f91bc",
Subtotal: Money{Amount: 19900, Currency: "KRW"},
Tax: Money{Amount: 1592, Currency: "KRW"},
Total: Money{Amount: 21492, Currency: "KRW"},
}
data, err := json.MarshalIndent(inv, "", " ")
if err != nil {
log.Fatalf("marshal invoice: %v", err)
}
fmt.Println(string(data))
}
// {
// "id": "inv_9a2f91bc",
// "subtotal": { "amount": 199, "currency": "KRW", "display": "KRW 199.00" },
// "tax": { "amount": 15.92, "currency": "KRW", "display": "KRW 15.92" },
// "total": { "amount": 214.92, "currency": "KRW", "display": "KRW 214.92" }
// }상태 열거형——문자열 표현
type OrderStatus int
const (
StatusPending OrderStatus = iota
StatusPaid
StatusShipped
StatusCancelled
)
var orderStatusNames = map[OrderStatus]string{
StatusPending: "pending",
StatusPaid: "paid",
StatusShipped: "shipped",
StatusCancelled: "cancelled",
}
func (s OrderStatus) MarshalJSON() ([]byte, error) {
name, ok := orderStatusNames[s]
if !ok {
return nil, fmt.Errorf("unknown order status: %d", s)
}
return json.Marshal(name)
}
type Order struct {
ID string `json:"id"`
Status OrderStatus `json:"status"`
}
o := Order{ID: "ord_3c7f", Status: StatusShipped}
data, _ := json.MarshalIndent(o, "", " ")
// {
// "id": "ord_3c7f",
// "status": "shipped"
// }MarshalJSON() 과 UnmarshalJSON() 을 함께 구현하세요. 마샬링만 구현하면, JSON을 통한 타입 라운드트립(직렬화→저장→역직렬화)에서 구조가 조용히 손실되거나 잘못된 타입이 반환될 수 있습니다. 두 메서드가 합쳐져서 타입이 JSON 라운드트립을 견딜 수 있다는 계약을 형성합니다.UUID——문자열로 직렬화
Go 표준 라이브러리에는 UUID 타입이 없습니다. 가장 흔한 선택은 github.com/google/uuid로, 이미 MarshalJSON() 를 구현하고 인용부호가 붙은 RFC 4122 문자열로 직렬화합니다. 원시 [16]byte 나 커스텀 ID 타입을 사용한다면, JSON 출력에 base64로 인코딩된 바이너리가 나타나지 않도록 직접 인터페이스를 구현하세요.
import (
"encoding/json"
"fmt"
"github.com/google/uuid"
)
type AuditEvent struct {
EventID uuid.UUID `json:"event_id"` // "550e8400-e29b-41d4-a716-446655440000"으로 직렬화
SessionID uuid.UUID `json:"session_id"`
Action string `json:"action"`
ActorID string `json:"actor_id"`
OccuredAt string `json:"occurred_at"`
}
event := AuditEvent{
EventID: uuid.New(),
SessionID: uuid.MustParse("550e8400-e29b-41d4-a716-446655440000"),
Action: "user.password_changed",
ActorID: "usr_7f3a91bc",
OccuredAt: "2026-03-10T14:22:00Z",
}
data, _ := json.MarshalIndent(event, "", " ")
fmt.Println(string(data))
// {
// "event_id": "a4b2c1d0-...",
// "session_id": "550e8400-e29b-41d4-a716-446655440000",
// "action": "user.password_changed",
// "actor_id": "usr_7f3a91bc",
// "occurred_at": "2026-03-10T14:22:00Z"
// }[16]byte 로 저장하면, encoding/json 은 그것을 base64 문자열——예를 들어 "VQ6EAOKbQdSnFkRmVUQAAA=="——로 인코딩합니다. 항상 적절한 UUID 타입을 사용하거나, 정식 하이픈이 붙은 문자열 형식을 출력하는 MarshalJSON() 을 구현하세요.json.MarshalIndent() 파라미터 참고
함수 시그니처는 func MarshalIndent(v any, prefix, indent string) ([]byte, error)。prefix 와 indent 는 모두 문자열 리터럴입니다——Python의 indent=4 같은 숫자 약어는 없습니다.
자주 쓰는 struct tag 옵션:
json.Indent() — 기존 JSON 바이트 재포맷
HTTP 응답 바디, Postgres jsonb 컬럼, 또는 os.ReadFile 로 읽은 파일 등 이미 []byte 의 JSON을 가지고 있다면, pretty-print하기 전에 struct를 정의하고 언마샬할 필요가 없습니다. json.Indent 는 원시 바이트를 직접 재포맷하여 들여쓰기된 출력을 bytes.Buffer 에 씁니다.
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
)
func main() {
// 업스트림 서비스로부터 온 원시 JSON 페이로드 시뮬레이션
raw := []byte(`{"trace_id":"tr_9a2f","service":"checkout","latency_ms":342,"error":null}`)
var buf bytes.Buffer
if err := json.Indent(&buf, raw, "", " "); err != nil {
log.Fatalf("indent: %v", err)
}
fmt.Println(buf.String())
}
// {
// "trace_id": "tr_9a2f",
// "service": "checkout",
// "latency_ms": 342,
// "error": null
// }마이크로서비스에서 제가 자주 쓰는 패턴은 구조화 로그에 쓰기 전에 json.Indent 를 호출하는 것입니다——추가 오버헤드는 무시할 수 있지만, 인시던트 대응 시 로그 항목의 가독성이 훨씬 향상됩니다. 이 함수는 HTTP 응답 로깅, 저장된 JSON 문자열의 pretty-print, struct 정의를 사용할 수 없는 포맷-온-리드 파이프라인에 특히 유용합니다.
func logResponse(logger *slog.Logger, statusCode int, body []byte) {
var pretty bytes.Buffer
if err := json.Indent(&pretty, body, "", " "); err != nil {
// 바디가 유효한 JSON이 아님——원시 그대로 기록
logger.Debug("upstream response", "status", statusCode, "body", string(body))
return
}
logger.Debug("upstream response", "status", statusCode, "body", pretty.String())
}json.Indent 는 공백을 삽입하는 데 필요한 구조 이상으로 JSON을 완전히 검증하지 않습니다. 완전한 구문 검증을 위해서는 먼저 json.Valid(data) 를 호출하고, false인 경우를 처리한 후 들여쓰기를 시도하세요.파일과 HTTP 응답에서 JSON 포맷하기
Go 서비스에서 가장 일반적인 두 가지 실제 시나리오는 디스크의 파일에서 읽은 JSON (설정 파일, 픽스처 데이터, 마이그레이션 시드)을 포맷하는 것과, 디버그 로깅이나 테스트 어서션을 위해 HTTP 응답 바디를 pretty-print하는 것입니다. 둘 다 같은 패턴을 따릅니다: 바이트를 읽고, json.Indent 를 호출하거나 언마샬하고 나서 json.MarshalIndent 를 호출한 다음, 다시 쓰거나 로그에 기록합니다.
파일 읽기 → 포맷 → 다시 쓰기
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"os"
)
func formatJSONFile(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read %s: %w", path, err)
}
if !json.Valid(data) {
return fmt.Errorf("invalid JSON in %s", path)
}
var buf bytes.Buffer
if err := json.Indent(&buf, data, "", " "); err != nil {
return fmt.Errorf("indent %s: %w", path, err)
}
if err := os.WriteFile(path, buf.Bytes(), 0644); err != nil {
return fmt.Errorf("write %s: %w", path, err)
}
return nil
}
func main() {
if err := formatJSONFile("config/database.json"); err != nil {
log.Fatalf("format config: %v", err)
}
fmt.Println("config/database.json 포맷 완료")
}HTTP 응답 → 디코드 → 디버그 로그용 Pretty-Print
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
)
type HealthResponse struct {
Status string `json:"status"`
Version string `json:"version"`
Checks map[string]string `json:"checks"`
UptimeSec int64 `json:"uptime_seconds"`
}
func main() {
resp, err := http.Get("https://api.payments.internal/v2/health")
if err != nil {
log.Fatalf("health check: %v", err)
}
defer resp.Body.Close()
var result HealthResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
log.Fatalf("decode health response: %v", err)
}
pretty, err := json.MarshalIndent(result, "", " ")
if err != nil {
log.Fatalf("marshal health response: %v", err)
}
fmt.Printf("헬스 체크 (%d):
%s
", resp.StatusCode, string(pretty))
}
// 헬스 체크 (200):
// {
// "status": "ok",
// "version": "1.4.2",
// "checks": {
// "database": "ok",
// "cache": "ok",
// "queue": "degraded"
// },
// "uptime_seconds": 172800
// }원시 응답 바디 → json.Indent(Struct 불필요)
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
)
func main() {
resp, err := http.Get("https://api.payments.internal/v2/health")
if err != nil {
log.Fatalf("request: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatalf("read body: %v", err)
}
var buf bytes.Buffer
if err := json.Indent(&buf, body, "", " "); err != nil {
log.Fatalf("indent: %v", err)
}
fmt.Println(buf.String())
}Go에서 HTTP 응답의 JSON을 Pretty Print하기
위의 두 가지 접근법이 가장 일반적인 경우를 커버합니다: 타입이 있는 struct로 디코딩한 후 json.MarshalIndent 를 호출(특정 필드를 검증하거나 검사할 필요가 있을 때 최선), 또는 io.ReadAll 로 원시 바디 바이트를 읽어 직접 json.Indent 를 호출(struct 정의가 없을 때 빠른 디버그 로깅에 최선). 원시 바이트 방식은 더 단순하지만 Go 타입 안전성이나 필드 접근을 제공하지 않습니다—— 순수하게 표시 변환입니다. 전체 응답 바디가 메모리에 들어가는 한, 두 가지 방식 모두 큰 응답 바디를 올바르게 처리합니다.
// 패턴 A: 타입 디코드 → MarshalIndent // 특정 필드를 검사하거나 검증해야 할 때 사용 var result map[string]any json.NewDecoder(resp.Body).Decode(&result) pretty, _ := json.MarshalIndent(result, "", " ") fmt.Println(string(pretty)) // 패턴 B: 원시 바이트 → json.Indent // 빠른 디버그 로깅에 사용——struct 정의 불필요 body, _ := io.ReadAll(resp.Body) var buf bytes.Buffer json.Indent(&buf, body, "", " ") fmt.Println(buf.String())
Go 프로젝트에서의 커맨드라인 JSON 포맷
때로는 Go 프로그램을 작성하지 않고 터미널에서 바로 JSON 페이로드를 포맷해야 합니다. 다음 원라이너들은 개발 중이나 인시던트 대응 시에 몸에 베어 있는 것들입니다.
echo '{"service":"payments","port":8443,"workers":4}' | python3 -m json.tool
# {
# "service": "payments",
# "port": 8443,
# "workers": 4
# }# 포맷만 cat api-response.json | jq . # 중첩 필드 추출 cat api-response.json | jq '.checks.database' # 배열 필터링 cat audit-log.json | jq '.[] | select(.severity == "error")'
# main.go: stdin 읽기, 포맷, stdout에 쓰기
cat <<'EOF' > /tmp/fmt.go
package main
import ("bytes";"encoding/json";"io";"os")
func main() {
b,_:=io.ReadAll(os.Stdin)
var buf bytes.Buffer
json.Indent(&buf,b,""," ")
os.Stdout.Write(buf.Bytes())
}
EOF
echo '{"port":8080,"debug":true}' | go run /tmp/fmt.gogofmt 는 Go 소스 코드를 포맷하는 것이지 JSON이 아닙니다. JSON 파일을 gofmt 에 파이프하지 마세요——에러가 나거나 알아볼 수 없는 출력이 생성됩니다. JSON 파일에는 jq . 또는 python3 -m json.tool 를 사용하세요.고성능 대안 — go-json
대부분의 Go 서비스에서 encoding/json 은 충분히 빠릅니다. 하지만 JSON 마샬링이 프로파일러에 나타난다면——고처리량 REST API나 매 요청마다 큰 구조화 로그 줄을 출력하는 서비스에서 흔함—— go-json 라이브러리는 동일한 API로 3~5배 빠른 드롭인 대체품입니다.
go get github.com/goccy/go-json
package main
import (
// 이것을 대체:
// "encoding/json"
// 이것으로——동일한 API, 다른 코드 변경 불필요:
json "github.com/goccy/go-json"
"fmt"
"log"
)
type AuditEvent struct {
RequestID string `json:"request_id"`
UserID string `json:"user_id"`
Action string `json:"action"`
ResourceID string `json:"resource_id"`
IPAddress string `json:"ip_address"`
DurationMs int `json:"duration_ms"`
}
func main() {
event := AuditEvent{
RequestID: "req_7d2e91", UserID: "usr_4421",
Action: "invoice.download", ResourceID: "inv_9a2f",
IPAddress: "203.0.113.45", DurationMs: 23,
}
data, err := json.MarshalIndent(event, "", " ")
if err != nil {
log.Fatalf("marshal: %v", err)
}
fmt.Println(string(data))
// 출력은 encoding/json과 동일——속도만 다름
}github.com/bytedance/sonic 은 가장 빠른 Go JSON 라이브러리이지만, amd64 와 arm64 에서만 동작합니다(JIT 컴파일 사용). 이식 가능한 드롭인 대체품이 필요할 때는 go-json 을 사용하고, 알려진 아키텍처에서 핫 경로의 마지막 마이크로초가 필요할 때 sonic 을 고려하세요.
대용량 JSON 파일 처리
json.MarshalIndent 와 json.Indent 는 모두 전체 페이로드가 메모리에 있어야 합니다. 데이터 내보내기, 감사 로그, Kafka 컨슈머 페이로드 등 100 MB를 초과하는 파일은 json.Decoder 를 사용해 입력을 스트리밍하고 레코드를 하나씩 처리하세요.
json.Decoder로 대용량 JSON 배열 스트리밍
package main
import (
"encoding/json"
"fmt"
"log"
"os"
)
type AuditEvent struct {
RequestID string `json:"request_id"`
UserID string `json:"user_id"`
Action string `json:"action"`
Severity string `json:"severity"`
DurationMs int `json:"duration_ms"`
}
func processAuditLog(path string) error {
file, err := os.Open(path)
if err != nil {
return fmt.Errorf("open %s: %w", path, err)
}
defer file.Close()
dec := json.NewDecoder(file)
// 여는 '[' 읽기
if _, err := dec.Token(); err != nil {
return fmt.Errorf("read opening token: %w", err)
}
var processed int
for dec.More() {
var event AuditEvent
if err := dec.Decode(&event); err != nil {
return fmt.Errorf("decode event %d: %w", processed, err)
}
// 한 번에 하나씩 처리——메모리 사용량 일정
if event.Severity == "error" {
fmt.Printf("[ERROR] %s: %s (%dms)
", event.UserID, event.Action, event.DurationMs)
}
processed++
}
fmt.Printf("%d개의 감사 이벤트 처리 완료
", processed)
return nil
}
func main() {
if err := processAuditLog("audit-2026-03.json"); err != nil {
log.Fatalf("process audit log: %v", err)
}
}NDJSON — 줄당 하나의 JSON 객체
package main
import (
"bufio"
"encoding/json"
"fmt"
"log"
"os"
)
type LogLine struct {
Timestamp string `json:"ts"`
Level string `json:"level"`
Service string `json:"service"`
Message string `json:"msg"`
TraceID string `json:"trace_id"`
DurationMs int `json:"duration_ms,omitempty"`
}
func main() {
file, err := os.Open("service-2026-03.ndjson")
if err != nil {
log.Fatalf("open log: %v", err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
scanner.Buffer(make([]byte, 1024*1024), 1024*1024) // 줄당 1 MB
for scanner.Scan() {
var line LogLine
if err := json.Unmarshal(scanner.Bytes(), &line); err != nil {
continue // 잘못된 줄 건너뛰기
}
if line.Level == "error" {
fmt.Printf("%s [%s] %s trace=%s
",
line.Timestamp, line.Service, line.Message, line.TraceID)
}
}
if err := scanner.Err(); err != nil {
log.Fatalf("scan: %v", err)
}
}os.ReadFile 로 500 MB JSON 파일을 로드하면 힙에 해당 버퍼 전체가 할당되어 GC 압력을 유발하고, 메모리가 제한된 컨테이너에서 OOM을 일으킬 수 있습니다.흔한 실수
문제: _로 에러를 버리면 직렬화 불가능한 값(채널, 함수, 복소수)이 조용히 nil 출력을 생성하거나, 하위에서 string(nil)을 호출할 때 패닉이 발생합니다.
해결: 항상 에러를 확인하세요. 항상 성공해야 하는 타입을 마샬링한다면, 패닉하는 log.Fatalf가 조용한 데이터 손실보다 낫습니다.
data, _ := json.MarshalIndent(payload, "", " ") fmt.Println(string(data)) // 마샬링 실패 시 빈 문자열
data, err := json.MarshalIndent(payload, "", " ")
if err != nil {
log.Fatalf("marshal payload: %v", err)
}
fmt.Println(string(data))문제: fmt.Println(string(data))는 JSON 뒤에 개행 문자를 추가해서, 출력을 원시 바이트로 처리하는 파이프라인을 망칩니다——예를 들어 jq에 파이프하거나 바이너리 프로토콜에 쓸 때.
해결: 바이너리 클린 출력에는 os.Stdout.Write(data)를 사용하세요. 사람이 보기 위한 후행 개행이 필요하면 명시적으로 추가하세요.
data, _ := json.MarshalIndent(cfg, "", " ") fmt.Println(string(data)) // 끝에 여분의 개행 추가
data, _ := json.MarshalIndent(cfg, "", " ")
os.Stdout.Write(data)
os.Stdout.Write([]byte("
")) // 필요할 때만 명시적 개행문제: omitempty 없이는 nil *string이나 *int 포인터가 "field": null로 직렬화됩니다. 이는 내부 필드명을 노출하고 컨슈머 측의 엄격한 JSON 스키마 검증기를 망가뜨릴 수 있습니다.
해결: 출력에서 존재하지 않기를(null이 아니라)원하는 포인터 필드에 omitempty를 추가하세요. omitempty를 가진 nil *T는 JSON에 키를 전혀 생성하지 않습니다.
type WebhookPayload struct {
EventID string `json:"event_id"`
ErrorMsg *string `json:"error_msg"` // nil일 때 null로 나타남
}
// {"event_id":"evt_3c7f","error_msg":null}type WebhookPayload struct {
EventID string `json:"event_id"`
ErrorMsg *string `json:"error_msg,omitempty"` // nil일 때 생략됨
}
// {"event_id":"evt_3c7f"}문제: map[string]any로 언마샬링하면 타입 정보가 손실되고, 수동 타입 단언이 필요하며, 비결정적인 키 순서가 생겨——JSON diff와 로그 비교가 더 어려워집니다.
해결: 적절한 json 태그를 가진 struct를 정의하세요. Struct는 타입 안전하고, 더 빠르게 마샬링되며, struct 정의와 일치하는 결정적인 필드 순서를 생성하고, 코드가 자기 문서화됩니다.
var result map[string]any json.Unmarshal(body, &result) port := result["port"].(float64) // 타입 단언 필요, 타입이 틀리면 패닉
type ServiceStatus struct {
Service string `json:"service"`
Port int `json:"port"`
Healthy bool `json:"healthy"`
}
var result ServiceStatus
json.Unmarshal(body, &result)
port := result.Port // 타입 있음, 안전, 빠름encoding/json vs 대안——빠른 비교
struct 정의를 제어하고 포맷된 출력이 필요한 모든 경우——설정 파일, 디버그 로깅, 테스트 픽스처, API 응답 로깅——에는 json.MarshalIndent 를 사용하세요. 이미 원시 바이트를 가지고 있고 Go 타입을 거치는 라운드트립 없이 공백만 추가하면 될 때는 json.Indent 를 사용하세요. 프로파일링에서 JSON 마샬링이 측정 가능한 병목임이 확인된 후에만 go-json 이나 sonic 으로 전환하세요——대부분의 서비스에서는 표준 라이브러리로도 충분합니다.
자주 묻는 질문
Go에서 JSON을 pretty print하려면?
encoding/json 패키지의 json.MarshalIndent(v, "", "\t")를 호출합니다——두 번째 인자는 줄별 접두사(보통 빈 문자열)이고, 세 번째 인자는 레벨별 들여쓰기입니다. 탭에는 "\t"를, 2칸 공백에는 " "를 전달하세요. 외부 라이브러리는 필요 없으며, encoding/json은 Go 표준 라이브러리에 포함되어 있습니다.
package main
import (
"encoding/json"
"fmt"
"log"
)
func main() {
config := map[string]any{
"service": "payments-api",
"port": 8443,
"region": "ap-northeast-2",
}
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
log.Fatalf("marshal: %v", err)
}
fmt.Println(string(data))
// {
// "port": 8443,
// "region": "ap-northeast-2",
// "service": "payments-api"
// }
}json.Marshal과 json.MarshalIndent의 차이점은?
json.Marshal은 공백 없이 압축된 한 줄 JSON을 생성합니다——바이트 수가 중요한 네트워크 전송에 최적입니다. json.MarshalIndent는 두 개의 추가 문자열 파라미터(prefix와 indent)를 받아 들여쓰기된 사람이 읽기 쉬운 출력을 생성합니다. 두 함수 모두 같은 값 타입을 받고 ([]byte, error)를 반환합니다. MarshalIndent의 유일한 비용은 출력 바이트가 약간 더 많아지는 것과 공백 삽입에 필요한 아주 미미한 추가 CPU 시간뿐입니다.
import "encoding/json"
type HealthCheck struct {
Status string `json:"status"`
Version string `json:"version"`
Uptime int `json:"uptime_seconds"`
}
h := HealthCheck{Status: "ok", Version: "1.4.2", Uptime: 86400}
compact, _ := json.Marshal(h)
// {"status":"ok","version":"1.4.2","uptime_seconds":86400}
pretty, _ := json.MarshalIndent(h, "", " ")
// {
// "status": "ok",
// "version": "1.4.2",
// "uptime_seconds": 86400
// }struct를 언마샬하지 않고 JSON []byte를 포맷하려면?
json.Indent(&buf, src, "", "\t")를 사용하세요. 이 함수는 기존 JSON []byte를 받아 들여쓰기된 버전을 bytes.Buffer에 씁니다——struct 정의 불필요, 타입 단언 불필요, Go 타입을 거치는 라운드트립도 없습니다. HTTP 응답 바디나 데이터베이스 컬럼 등 이미 원시 JSON 바이트를 가지고 있을 때 가장 빠른 옵션입니다.
import (
"bytes"
"encoding/json"
"fmt"
"log"
)
raw := []byte(`{"endpoint":"/api/v2/invoices","page":1,"per_page":50,"total":312}`)
var buf bytes.Buffer
if err := json.Indent(&buf, raw, "", " "); err != nil {
log.Fatalf("indent: %v", err)
}
fmt.Println(buf.String())
// {
// "endpoint": "/api/v2/invoices",
// "page": 1,
// "per_page": 50,
// "total": 312
// }json.MarshalIndent가 에러를 반환하는 이유는?
값을 JSON으로 표현할 수 없을 때 encoding/json은 에러를 반환합니다. 가장 흔한 원인은: 채널, 함수, 또는 복소수 마샬링(이들은 JSON에 대응하는 표현이 없음), 에러를 반환하는 MarshalJSON()을 구현한 struct, 또는 문자열이 아닌 키를 가진 map입니다. 중요한 점은 익스포트되지 않은 필드나 nil 포인터 필드를 가진 struct 마샬링은 에러를 일으키지 않는다는 것입니다——그것들은 단순히 생략됩니다.
import (
"encoding/json"
"fmt"
)
// 이것은 에러를 반환——채널은 JSON 직렬화 불가
ch := make(chan int)
_, err := json.MarshalIndent(ch, "", " ")
fmt.Println(err)
// json: unsupported type: chan int
// 이것은 문제없음——omitempty를 가진 nil 포인터 필드는 조용히 생략됨
type Profile struct {
ID string `json:"id"`
Avatar *string `json:"avatar,omitempty"`
}
p := Profile{ID: "usr_7b3c"}
data, _ := json.MarshalIndent(p, "", " ")
// {"id": "usr_7b3c"} — avatar 생략, 에러 없음Go에서 JSON 출력에서 필드를 제외하려면?
세 가지 방법이 있습니다. 첫째, json:"-" struct tag를 사용——필드 값에 관계없이 항상 제외됩니다. 둘째, omitempty 사용——필드가 해당 타입의 zero value(nil 포인터, 빈 문자열, 0, false)를 가질 때만 제외됩니다. 셋째, 익스포트되지 않은(소문자로 시작하는)필드는 태그 없이 encoding/json에 의해 자동으로 제외됩니다.
type PaymentMethod struct {
ID string `json:"id"`
Last4 string `json:"last4"`
ExpiryMonth int `json:"expiry_month"`
ExpiryYear int `json:"expiry_year"`
CVV string `json:"-"` // 항상 제외
BillingName string `json:"billing_name,omitempty"` // 비어있으면 제외
internalRef string // 익스포트 안됨——자동 제외
}
pm := PaymentMethod{
ID: "pm_9f3a", Last4: "4242",
ExpiryMonth: 12, ExpiryYear: 2028,
CVV: "123", internalRef: "stripe:pm_9f3a",
}
data, _ := json.MarshalIndent(pm, "", " ")
// CVV, BillingName(빈 값), internalRef는 출력에 나타나지 않음JSON 마샬링에서 time.Time을 어떻게 처리하나요?
encoding/json은 기본적으로 time.Time을 RFC3339Nano 형식(예: "2026-03-10T14:22:00Z")으로 마샬링하며, ISO 8601 호환입니다. 레거시 API를 위한 Unix 에포크 정수나 커스텀 날짜 문자열 등 다른 형식이 필요하다면, time.Time을 임베드하고 필요한 형식을 반환하는 래퍼 타입에 MarshalJSON()을 구현하세요.
import (
"encoding/json"
"fmt"
"time"
)
// 기본 동작——RFC3339Nano, 커스텀 코드 불필요
type AuditEvent struct {
Action string `json:"action"`
OccurredAt time.Time `json:"occurred_at"`
}
e := AuditEvent{
Action: "invoice.paid",
OccurredAt: time.Date(2026, 3, 10, 14, 22, 0, 0, time.UTC),
}
data, _ := json.MarshalIndent(e, "", " ")
// {
// "action": "invoice.paid",
// "occurred_at": "2026-03-10T14:22:00Z"
// }
// 커스텀: Unix 타임스탬프(정수)
type UnixTime struct{ time.Time }
func (u UnixTime) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf("%d", u.Unix())), nil
}관련 도구
James is a systems engineer and Go enthusiast who focuses on high-performance microservices, command-line tooling, and infrastructure automation. He enjoys the simplicity and explicitness of Go and writes about building fast, reliable backend systems. When not coding he explores distributed systems concepts and contributes to open-source Go libraries.
Tobias is a platform engineer who builds developer tooling and internal infrastructure in Go. He has authored several open-source CLI tools and contributes to the Go toolchain ecosystem. He writes about the cobra and urfave/cli frameworks, cross-platform binary distribution, configuration management, and the patterns that make Go an ideal language for building reliable, self-contained command-line utilities.