DOM과 SAX, 파서 내부 동작과 에러 복구 전략으로 고르는 법
DOM과 SAX 두 XML 파서를 내부 동작 원리·에러 복구 정책·보안 관점에서 비교하고, Java·Python 코드 예시로 실무 선택 기준을 정리한다.
2026-08-13 · 최초 발행 2025-12-03
파서가 문서를 쥐는 방식이 다르다
XML을 애플리케이션이 읽어 들이는 방식은 크게 두 갈래다. DOM(Document Object Model)은 문서를 메모리에 트리 구조로 완전히 로드해 임의 접근(random access)을 가능하게 하고, XPath 탐색이나 변환·검증 작업에 유리하다. SAX(Simple API for XML)는 문서를 순차적으로 읽으며 요소의 시작·텍스트·종료 시점마다 콜백으로 이벤트를 전달하는 스트리밍 기반 이벤트 드리븐 모델이다.
선택은 결국 트레이드오프의 문제다. DOM은 탐색·수정이 편하고 API가 풍부하지만 메모리 사용량이 크고, SAX는 메모리가 적고 확장성이 높은 대신 상태 관리와 코드 복잡도가 늘어난다.
DOM은 트리를 통째로 올려 둔다
DOM은 문서 전체를 트리 형태로 상주시킨다. 대략 입력 크기의 수배(구조체·객체 오버헤드 포함)에 달하는 메모리를 쓰기 때문에 대용량 문서에는 불리하지만, 노드 참조를 재사용하고 캐시하면 재탐색이 빨라진다. XPath/XQuery, 노드 삽입·삭제·이동, 스키마 검증 같은 고급 작업을 지원해 XSLT 변환이나 템플릿 처리에 적합하고, 구조가 눈에 보이는 트리이기 때문에 테스트·로깅·단위 검증도 단순해진다.
SAX는 이벤트만 흘려보낸다
SAX는 메모리를 현재 요소의 깊이와 핸들러 상태만큼만 유지한다 — 입력 크기와 무관하게 O(depth) 수준으로 소비하기 때문에 파일 크기가 수GB에 달해도 안정적으로 동작한다. startElement/characters/endElement 이벤트로 상태 머신을 설계해 처리 파이프라인을 구성하고, 다운스트림 큐·배압(backpressure)과 결합하면 실시간 ETL을 만들 수 있다. 다만 상태 관리와 버퍼링이 필요하고 중첩 구조·혼합 콘텐츠 처리 난이도가 있으며, 임의 접근이 불가능해 재참조가 필요하면 별도 인덱싱이나 2-pass 전략을 둬야 한다.
파서 내부는 같은 길을 걷다가 갈라진다
두 모델 모두 파서 엔진은 토큰화(lexing)에서 시작해 구문 분석(parsing)을 거친 뒤 노드를 생성하거나 이벤트를 발생시킨다. 이 과정에서 문자셋 디코딩, 네임스페이스 처리, 엔티티 확장이 함께 일어난다. 이후 갈리는 지점이 핸들러다 — DOM은 Node/Element/Text 객체 그래프를 만들고, SAX는 ContentHandler/ErrorHandler로 스트림 이벤트를 처리한다.
검증 단계도 선택 사항으로 걸 수 있다. DTD/XSD 검증 옵션을 켜면 well-formedness를 위반한 문서에서 예외가 발생하는데, 이때 재시도·스킵·장애 전파·알림 연계 중 어떤 정책을 쓸지는 파이프라인 설계자의 몫이다.
실무에서는 이렇게 갈린다
수GB 단위 XML 로그를 스트리밍으로 JSON 변환해 메시지 큐에 적재하는 대용량 로그·트레이스 수집 파이프라인은 SAX가 맞다. 반대로 수십 KB~수 MB 규모 설정 파일을 로드해 XPath로 조건 검색·변경하는 설정·메타데이터 관리는 DOM이 편하다. XML→DB ETL처럼 레코드 단위 upsert에 배압을 적용하고 부분 실패를 스킵하는 구조라면 SAX와 배치 커밋을 묶는다. XSLT 변환, XSD 검증, 레거시 문서 구조 리팩토링 같은 문서 변환·검증 작업은 DOM을 선호하고, 메모리가 제한된 모바일·임베디드 클라이언트에서 피드를 파싱하고 리스트를 구성할 때는 SAX나 Pull 방식(StAX 유사)을 쓴다.
지표로 나란히 놓으면
| 지표 | DOM | SAX |
|---|---|---|
| 성능 | 소형~중형 문서에 유리, 재탐색/변환 빠름 | 대형 문서 스트리밍 처리, 초기 레코드 지연 최소화 |
| 확장성 | 문서 크기 증가 시 힙 압박·GC 부담 | 입력 크기와 무관한 저메모리, 수GB 문서 처리 가능 |
| 일관성 | 트리 일관성 유지 용이, 트랜잭션적 수정 가능 | 이벤트 처리 중 상태 불일치 위험, 철저한 상태 관리 필요 |
| 안정성 | OOM/GC 스톨 가능성, 예외 시 전체 영향 | 부분 실패 스킵·재시도 용이, 백프레셔와 결합 유리 |
| 운영 편의 | 디버깅/로깅·XPath 지원, 도구 풍부 | 핸들러 로깅·상태 추적 필요, 러닝커브 존재 |
500 MB XML을 기준으로 삼으면 DOM은 환경에 따라 메모리를 2~6배까지 쓸 수 있는 반면 SAX는 문서 깊이에 비례해 수십 MB 이내로 유지된다(수치 범위는 구현체·문서 구조에 따라 달라진다). 처리 지연에서도 차이가 난다 — SAX는 스트리밍이라 초기 레코드부터 즉시 전달하지만 DOM은 전체 파싱이 끝나야 처리를 시작한다. 운영 안정성 측면에서 SAX는 파일 크기 증가에 선형으로 대응해 OOM 위험이 낮은 반면, DOM은 JVM 힙과 GC 압력이 함께 늘어난다. 개발 비용은 반대로 움직인다 — DOM은 구현이 단순하고 검증이 쉬운 대신, SAX는 상태 머신을 설계하는 비용이 들지만 장기적으로 대용량 처리에서는 운영비를 줄인다.
판단은 이 흐름을 탄다
코드로 보면
Java 11 이상과 Python 3.10 이상 표준 라이브러리로 동작하는 최소 예시다.
SAX는 콜백에서 상태를 들고 있다가 종료 시점에 카운트를 마무리한다.
// javac Main.java && java Main input.xml
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.*;
public class Main {
public static void main(String[] args) throws Exception {
SAXParserFactory f = SAXParserFactory.newInstance();
f.setNamespaceAware(true);
SAXParser p = f.newSAXParser();
DefaultHandler h = new DefaultHandler() {
private StringBuilder buf = new StringBuilder();
private int itemCount = 0;
public void startElement(String uri, String local, String qName, Attributes atts) {
buf.setLength(0);
}
public void characters(char[] ch, int start, int length) {
buf.append(ch, start, length);
}
public void endElement(String uri, String local, String qName) {
if ("item".equals(local) || "item".equals(qName)) itemCount++;
}
public void endDocument() { System.out.println("items=" + itemCount); }
};
p.parse(args[0], h);
}
}
DOM은 트리를 만든 뒤 findall로 노드를 뽑고, 필요하면 속성을 바꿔 다시 저장한다.
# python dom_example.py input.xml
import sys
import xml.etree.ElementTree as ET
tree = ET.parse(sys.argv[1])
root = tree.getroot()
items = root.findall(".//item")
print(f"items={len(items)}")
# 노드 수정 예시: 첫 item에 속성 추가 후 저장
if items:
items[0].set("processed", "true")
tree.write("output.xml", encoding="utf-8", xml_declaration=True)
선택 기준과 운영상의 함정
선택은 입력 크기, 지연 요구, 임의 탐색 필요성, 메모리 한계로 결정한다. 메타데이터는 DOM으로, 페이로드는 SAX로 처리하는 혼합 전략도 고려할 만하다. 에러·복구 전략은 검증 수준(DTD/XSD)을 단계화하고, 복구 가능한 오류는 스킵·격리 큐로 보내고 복구 불가능한 오류는 fail-fast와 알림으로 연결한다. 성능 튜닝은 SAX라면 배치 크기·큐 수용량·백프레셔 설정을, DOM이라면 힙 사이즈·GC 튜닝·노드 복제 최소화를 다룬다.
보안에서는 XXE(외부 엔티티) 비활성화가 기본이다. 엔티티 확장 폭탄을 막고, 신뢰할 수 없는 입력에는 사이즈·깊이 제한을 걸어야 한다.
데이터 특성과 SLO를 기준으로 혼합 아키텍처를 구성하고, 네임스페이스·검증·보안 플래그 같은 표준 파서 옵션을 명시적으로 지정하며 에러 정책과 모니터링을 파이프라인에 내장하는 편이 두 모델 모두에서 안전하다.