Engineering Note

수정하지 않을 YAML 로그를 왜 전부 객체로 만들까

초당 수십/수백 건의 YAML 로그를 선별하던 경험을 바탕으로, 로그처럼 immutable한 데이터를 처리할 때 DOM/object tree 대신 stream/event 기반 처리를 고려해야 하는 이유를 정리합니다.

2026년 6월 23일 · Pletor Engineering yamlloggingperformancestreamingoperations

YAML을 처리해야 하면 자연스럽게 떠오르는 방식이 있습니다. 파일을 읽고, parser로 넘기고, 결과를 object나 map으로 만든 다음 필요한 값을 꺼냅니다.

작은 설정 파일에서는 이 방식이 편합니다. 코드도 읽기 쉽고, 특정 key를 찾기도 쉽습니다. 값을 고치거나 구조를 다시 써야 한다면 전체 tree를 갖고 있는 편이 오히려 자연스럽습니다.

하지만 모든 YAML 처리가 설정 파일 편집은 아닙니다. 제가 겪었던 문제는 로그 처리 쪽에 가까웠습니다.

초당 수십 건에서 많게는 수백 건의 YAML 포맷 로그가 계속 들어왔고, 애플리케이션은 그중 일부만 선별해야 했습니다. 로그는 발생한 뒤에는 immutable한 데이터였습니다. 내용을 고치지 않았고, 다시 예쁘게 출력하지도 않았고, 전체 구조를 도메인 객체로 오래 들고 있을 이유도 없었습니다.

필요한 일은 이 정도였습니다.

로그 한 건을 읽는다.
몇 개의 field를 확인한다.
조건에 맞으면 다음 단계로 넘긴다.
아니면 버린다.

그런데 처음 구현은 매번 YAML 전체를 object tree로 만들었습니다.

수많은 반투명 로그 문서가 스트림처럼 흐르고 일부 이벤트만 따뜻한 빛으로 선별되는 일러스트
immutable 로그를 선별하는 작업이라면, 모든 이벤트를 전체 object tree로 만들 필요가 없을 수 있습니다.

처음에는 당연히 객체로 만들었다

로그 한 건은 대략 이런 모양이었습니다.

timestamp: "2026-06-23T10:15:30Z"
service: payment-api
level: WARN
event:
  type: dependency_timeout
  target: settlement-db
  elapsed_ms: 1280
trace:
  id: 7f9d0b7e8f0a4c31
  sampled: true
labels:
  region: ap-northeast-2
  tenant: enterprise-a

처음에는 이 로그를 파싱해서 map이나 object로 만들고, level, service, event.type, trace.sampled 같은 값을 꺼내 조건을 판단했습니다.

이 방식은 익숙합니다. 테스트하기도 쉽습니다. 특정 필드가 빠졌을 때 처리하기도 편합니다.

문제는 처리량이 올라가면서 보였습니다. 애플리케이션은 로그를 수정하지 않는데도 매 로그마다 전체 YAML 구조를 만들고 있었습니다. nested map, list, scalar value, wrapper object가 계속 생겼고, 선별 조건에 필요 없는 field까지 모두 메모리에 올라왔습니다.

한 건씩 보면 작아 보입니다. 하지만 초당 수십/수백 건이 계속 들어오면 이야기가 달라집니다. object allocation이 늘고, short-lived object가 쌓이고, GC가 더 자주 움직입니다. CPU 시간도 parsing 자체뿐 아니라 tree를 만들고 버리는 데 쓰입니다.

그때 기준을 다시 잡았습니다.

이 데이터는 수정되는가?
전체 구조가 필요한가?
필요한 field를 확인한 뒤 바로 버려도 되는가?

로그라면 대답이 꽤 명확했습니다. 발생 이후의 로그는 수정 대상이 아니라 판단 대상이었습니다.

DOM은 편하지만, 공짜는 아니다

여기서 DOM이라는 표현은 XML의 Document Object Model만을 뜻하지 않습니다. YAML 처리에서도 전체 문서를 node tree, map/list 구조, 혹은 도메인 객체로 만드는 방식을 넓게 DOM 방식이라고 부르겠습니다.

DOM 방식의 장점은 분명합니다.

  • 전체 구조를 여러 번 탐색할 수 있습니다.
  • parent/child 관계를 자유롭게 오갈 수 있습니다.
  • 값을 수정하고 다시 출력하기 쉽습니다.
  • schema validation이나 복잡한 변환을 구현하기 쉽습니다.
  • 코드가 직관적인 경우가 많습니다.

하지만 로그 선별처럼 읽고 버리는 작업에서는 이 장점이 전부 필요하지 않을 수 있습니다.

필요한 값이 level, event.type, service 세 개뿐인데, 매번 전체 tree를 만든다면 실제 목적보다 큰 일을 하는 셈입니다.

필요한 작업: 조건 판단
실제 작업: 전체 YAML 문서를 객체 그래프로 구성한 뒤 조건 판단

이 차이가 처리량이 낮을 때는 잘 보이지 않습니다. 하지만 입력이 계속 들어오고, 로그 한 건의 크기가 커지고, 필터 조건은 단순한 상태가 유지되면 비용이 드러납니다.

Stream 방식으로 관점을 바꾸기

stream/event 기반 처리는 YAML 문서를 전체 객체로 만들기보다 parser가 읽어 내려가며 발생시키는 이벤트를 소비하는 방식입니다.

개념적으로는 이런 흐름입니다.

document start
mapping start
scalar: timestamp
scalar: 2026-06-23T10:15:30Z
scalar: service
scalar: payment-api
scalar: level
scalar: WARN
...
mapping end
document end

처리 애플리케이션은 이벤트가 흐르는 동안 현재 path를 추적하고, 관심 있는 field를 만나면 값만 확인합니다.

예를 들면 이런 식입니다.

level == WARN 또는 ERROR 인가?
event.type 이 dependency_timeout 인가?
service 가 관심 대상인가?
trace.sampled 가 true 인가?

조건 판단에 필요한 값이 모이면 더 이상 전체 문서를 object로 만들 필요가 없습니다. 구현에 따라서는 남은 이벤트를 빠르게 흘려보내거나, 다음 로그로 넘어갈 수도 있습니다.

핵심은 “YAML을 덜 파싱한다”가 아닙니다. YAML 문법을 이해하려면 여전히 parser가 필요합니다. 차이는 parser 이후에 전체 object graph를 만들지 않는다는 데 있습니다.

로그는 immutable하다는 사실

이 문제에서 가장 중요한 단서는 YAML이 아니라 로그의 성격이었습니다.

로그는 발생 이후에 의미가 고정됩니다. downstream에서 로그 본문을 고치는 일은 보통 없습니다. 필요한 일은 선별, 라우팅, 집계, 일부 field 추출, alert 조건 판단에 가깝습니다.

설정 파일: 읽고, 이해하고, 수정하고, 다시 쓴다.
로그 이벤트: 읽고, 판단하고, 넘기거나 버린다.

둘 다 YAML일 수 있지만 처리 모델은 달라야 합니다.

설정 파일은 DOM 방식이 편한 경우가 많습니다. 사용자가 값을 바꾸고, default를 채우고, 일부 구조를 재작성하고, 결과를 다시 저장해야 할 수 있습니다.

반대로 로그 이벤트는 전체 구조를 오래 들고 있을 이유가 적습니다. 필터 조건을 만족하는지만 확인하면 됩니다. 이때 stream/event 방식은 데이터의 생애주기와 잘 맞습니다.

성능 차이가 생기는 지점

stream 방식의 이점은 주로 네 가지에서 나옵니다.

첫째, 메모리 사용량이 줄어듭니다. 전체 문서를 tree로 만들지 않으므로 로그 한 건마다 생성되는 객체 수가 줄어듭니다.

둘째, GC 부담이 줄어듭니다. 로그 처리 애플리케이션에서는 대부분의 object가 아주 짧게 살다 사라집니다. short-lived object가 많아지면 GC는 계속 바빠집니다.

셋째, 필요한 field만 집중해서 볼 수 있습니다. 모든 값을 application-level object로 변환하지 않아도 됩니다.

넷째, backpressure를 설계하기 쉬워집니다. 입력 stream, parsing, filtering, output을 단계로 나누면 어느 지점에서 밀리는지 보기 쉬워집니다.

물론 작은 파일 몇 개를 처리할 때는 큰 차이가 없을 수 있습니다. 오히려 DOM 방식이 더 단순하고 유지보수하기 좋을 수도 있습니다. stream 방식은 상태를 직접 추적해야 하므로 코드가 더 섬세해집니다.

따라서 기준은 “stream이 항상 빠르다”가 아닙니다.

수정이 필요한가?
전체 구조가 필요한가?
랜덤 접근이 필요한가?
입력량이 충분히 큰가?
필터 조건이 제한적인가?

이 질문에 따라 선택해야 합니다.

YAML이라서 조심할 점

YAML은 JSON보다 문법이 넓습니다. indentation, sequence, mapping, scalar style, multi-document, anchor, alias, merge key 같은 요소가 있습니다.

stream 방식으로 처리할 때는 parser 이벤트만 보고 path를 추적해야 하므로 구현이 단순한 line scanner처럼 흘러가면 위험합니다. 특히 운영 로그가 사람이 작성한 YAML이 아니라 시스템에서 생성한 YAML이라면 포맷 범위를 제한하는 것이 좋습니다.

예를 들어 로그 생산자가 다음을 보장한다면 소비자는 훨씬 단순해집니다.

  • multi-document stream을 쓰지 않는다.
  • anchor와 alias를 쓰지 않는다.
  • merge key를 쓰지 않는다.
  • 관심 field의 path를 안정적으로 유지한다.
  • scalar 표현을 가능한 한 단순하게 유지한다.

이런 제약을 둘 수 없다면 stream 처리는 더 조심해야 합니다. 그래도 전체 tree를 만들지 않는다는 선택은 여전히 가능하지만, YAML parser가 제공하는 event model과 edge case를 제대로 이해해야 합니다.

Java에서는 SnakeYAML의 event stream을 볼 수 있다

제가 이 문제를 다룰 때는 Java 애플리케이션에서 SnakeYAML을 사용했습니다. SnakeYAML을 단순히 Map이나 객체로 로드하는 라이브러리로만 보면 stream 처리 가능성을 놓치기 쉽습니다.

SnakeYAML에는 문서를 곧바로 Java object로 만드는 API뿐 아니라 parsing event를 순서대로 다루는 경로도 있습니다. classic SnakeYAML의 Yaml#parse(...)는 YAML 입력을 Event sequence로 노출하고, 내부적으로도 stream start, document start, mapping start, scalar, sequence start/end 같은 이벤트 모델을 사용합니다.

로그 선별에서는 이 모델이 잘 맞았습니다.

scalar key를 만난다.
현재 path를 갱신한다.
관심 field이면 다음 scalar value를 확인한다.
조건 판단에 필요한 값만 보관한다.

이 방식은 DOM처럼 편하지 않습니다. path tracking, sequence 안의 위치, nested mapping 상태를 직접 관리해야 합니다. 하지만 로그처럼 구조를 수정하지 않고 몇 개 field만 보는 경우에는 전체 Map<String, Object>를 만들지 않아도 된다는 장점이 있습니다.

즉, SnakeYAML을 쓴다고 해서 반드시 DOM/object tree 방식으로 가야 하는 것은 아닙니다. 라이브러리의 어느 계층을 사용할지 선택할 수 있고, 로그 처리처럼 throughput과 allocation이 중요한 곳에서는 event 계층을 검토할 가치가 있습니다.

JSON에도 같은 기준이 적용된다

이 이야기는 YAML에만 갇히지 않습니다. JSON도 마찬가지입니다.

JSON에서도 흔한 선택은 전체 문서를 object tree로 만드는 것입니다. Jackson이라면 ObjectMapper로 domain object에 binding하거나 JsonNode tree를 만들 수 있습니다. 편하고 강력합니다.

하지만 JSON 로그를 고치지 않고 몇 개 field만 보고 선별한다면 Jackson Streaming API를 사용할 수 있습니다. JsonParser가 token을 순서대로 내보내고, 애플리케이션은 필요한 field name과 value token만 확인합니다.

포맷은 다르지만 기준은 같습니다.

전체 문서를 조작해야 하면 tree나 binding이 편하다.
지나가는 이벤트에서 일부 값만 확인하면 stream/token model이 잘 맞을 수 있다.

YAML이든 JSON이든 핵심은 “형식 데이터니까 일단 객체로 만든다”가 아닙니다. 데이터가 어떻게 쓰이는지, 수정되는지, 얼마나 많이 들어오는지를 먼저 봐야 합니다.

DOM이 맞는 경우

이 글이 DOM 방식을 피하자는 이야기는 아닙니다. DOM은 여전히 좋은 기본값일 수 있습니다.

다음 상황에서는 전체 tree를 만드는 편이 낫습니다.

  • YAML 내용을 수정해야 한다.
  • 여러 위치의 값을 조합해 복잡한 판단을 해야 한다.
  • 같은 문서를 여러 번 탐색해야 한다.
  • schema validation 결과를 사용자에게 자세히 보여줘야 한다.
  • comment, ordering, formatting을 보존하거나 재출력해야 한다.
  • 처리량보다 구현 단순성이 더 중요하다.

중요한 것은 파일 포맷이 아니라 작업의 성격입니다.

편집해야 하는 YAML인가?
아니면 지나가는 이벤트인가?

이 차이가 처리 모델을 결정합니다.

운영 관점에서 본 선택

로그 처리 애플리케이션의 성능 문제는 종종 저장소, 네트워크, queue, downstream 처리량에서 시작하는 것처럼 보입니다. 하지만 입력을 받아 첫 번째 판단을 내리는 코드 안에서도 비용이 쌓일 수 있습니다.

특히 로그처럼 계속 들어오고, 대부분은 버려지고, 일부만 다음 단계로 넘어가는 데이터라면 “한 건을 얼마나 친절하게 객체화할 것인가”보다 “얼마나 빨리 판단하고 흘려보낼 것인가”가 더 중요할 때가 있습니다.

Konduo가 리소스와 메트릭, 알림을 연결해서 운영 상태를 보려는 것도 같은 맥락에 있습니다. 문제는 한 지표나 한 계층에서만 생기지 않습니다. 로그 처리 파이프라인에서도 parser, allocation, GC, queue, output sink가 함께 움직입니다. 처리 모델을 고를 때도 이 전체 흐름을 같이 봐야 합니다.

결론

YAML을 처리한다고 해서 항상 전체 문서를 객체로 만들어야 하는 것은 아닙니다.

설정 파일처럼 수정하고 다시 써야 하는 YAML이라면 DOM 방식이 자연스럽습니다. 하지만 로그처럼 발생 이후 immutable하고, 목적이 선별과 라우팅이라면 stream/event 기반 처리가 더 잘 맞을 수 있습니다.

제가 얻은 기준은 단순했습니다.

수정하지 않을 데이터라면, 먼저 전부 객체로 만들 필요가 있는지 의심해본다.

YAML 로그는 읽고 판단한 뒤 흘려보내는 데이터일 수 있습니다. 그렇다면 처리 모델도 그 생애주기에 맞춰야 합니다.

함께 읽기 좋은 글

이 글의 관점과 이어지는 주제를 더 보려면 아래 글도 함께 읽어볼 수 있습니다.