1. DOM (Document Object Model)
때로는 XML 파일이 너무 커서 통째로 메모리에 올릴 수 없을 때가 있습니다. 문서 구조가 미리 알려지지 않았거나 매우 복잡한 경우도 있고, 전체 파일이 아니라 데이터의 일부만 처리하면 되거나, 실행 중에 문서를 변경(노드 삭제, 새 요소 추가, 구조 일부 재구성)해야 할 수도 있습니다.
이런 상황에서는 오래되고 검증된 도구인 DOM과 SAX가 잘 맞습니다. 이들로 미리 정의된 Java 클래스에 묶이지 않고 XML의 내용을 직접 다룰 수 있습니다.
DOM의 동작 방식
DOM은 XML 문서를 메모리에서 객체 트리 형태로 표현하는 방식입니다. 각 태그는 트리의 노드(Node)가 되고, 속성, 텍스트 값, 심지어 주석까지도 별도의 객체가 됩니다. 문서를 로드한 후에는 구조 전체에 완전하게 접근할 수 있어 읽기, 수정, 삭제, 요소와 속성 추가가 가능합니다.
Java에서의 DOM 주요 클래스
- DocumentBuilderFactory — 파서를 생성하는 팩토리.
- DocumentBuilder — XML을 트리로 변환하는 파서.
- Document — 트리의 루트 객체.
- Element — XML 요소(태그).
- NodeList — 노드 목록(예: <items> 안의 모든 <item>).
예: DOM으로 XML 파일 읽기
다음과 같은 XML 파일을 읽어야 한다고 가정해 봅시다:
<contacts>
<person id="1">
<name>Ivan</name>
<email>ivan@example.com</email>
</person>
<person id="2">
<name>Mariya</name>
<email>mariya@example.com</email>
</person>
</contacts>
코드: 요소 읽기와 순회
import javax.xml.parsers.*;
import org.w3c.dom.*;
import java.io.File;
public class DomExample {
public static void main(String[] args) throws Exception {
// 1. 팩토리와 파서를 생성
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
// 2. XML 파일을 메모리로 로드
Document doc = builder.parse(new File("contacts.xml"));
// 3. 루트 요소 가져오기
Element root = doc.getDocumentElement();
System.out.println("루트 요소: " + root.getTagName());
// 4. 모든 <person> 목록 가져오기
NodeList persons = root.getElementsByTagName("person");
for (int i = 0; i < persons.getLength(); i++) {
Element person = (Element) persons.item(i);
String id = person.getAttribute("id");
String name = person.getElementsByTagName("name").item(0).getTextContent();
String email = person.getElementsByTagName("email").item(0).getTextContent();
System.out.println("id: " + id + ", name: " + name + ", email: " + email);
}
}
}
여기서 하는 일:
- 먼저 파서를 생성하고 XML 파일을 로드합니다.
- 루트 요소(contacts)를 가져옵니다.
- <person> 요소를 모두 찾아 순회합니다.
- 각 <person>의 id 속성과 <name>, <email> 요소를 읽습니다.
예제의 DOM 트리 구조
contacts
├── person (id="1")
│ ├── name ("Ivan")
│ └── email ("ivan@example.com")
└── person (id="2")
├── name ("Mariya")
└── email ("mariya@example.com")
DOM으로 XML 변경
DOM은 읽기뿐만 아니라 XML을 변경할 수도 있습니다. 예를 들어, 새 사람을 추가해 보겠습니다:
// 새 <person> 요소 생성
Element newPerson = doc.createElement("person");
newPerson.setAttribute("id", "3");
// <name>
Element name = doc.createElement("name");
name.setTextContent("Sergey");
newPerson.appendChild(name);
// <email>
Element email = doc.createElement("email");
email.setTextContent("sergey@example.com");
newPerson.appendChild(email);
// 루트에 추가
root.appendChild(newPerson);
// 변경 내용을 파일로 저장
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
transformer.transform(new DOMSource(doc), new StreamResult(new File("contacts-updated.xml")));
DOM의 주요 장단점
- 장점: 작은 파일에 적합하고, 어떤 변경이든 쉽게 할 수 있으며, 트리를 어느 방향으로든 “자유롭게” 탐색할 수 있습니다.
- 단점: 전체 XML을 메모리에 보관합니다. 큰 파일(수백 MB 이상)에는 부적합하며 메모리가 금방 바닥납니다.
2. SAX (Simple API for XML)
SAX는 이벤트 기반 파서입니다. 트리를 만들지 않고 XML을 “왼쪽에서 오른쪽으로” 읽으면서 “요소 시작”, “텍스트 내용”, “요소 종료” 같은 이벤트를 발생시킵니다. 필요한 이벤트에 반응하도록 핸들러를 작성하면 됩니다.
비유:
DOM이 다이어리의 모든 페이지를 책상 위에 펼쳐 놓는 것이라면, SAX는 다이어리를 페이지 단위로 읽으면서 필요한 페이지를 만났을 때만 메모하는 것과 같습니다.
Java에서의 SAX 주요 클래스
- SAXParserFactory, SAXParser — 팩토리와 파서.
- DefaultHandler — 이벤트 처리를 위한 기본 클래스.
- 핸들러 메서드: startElement, characters, endElement, startDocument, endDocument.
예: SAX로 XML 파일 읽기
같은 파일 contacts.xml이라고 가정합시다. 모든 사람의 이름과 e-mail만 출력하고 싶습니다.
코드: SAX 파서
import javax.xml.parsers.*;
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;
import java.io.File;
public class SaxExample {
public static void main(String[] args) throws Exception {
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser parser = factory.newSAXParser();
parser.parse(new File("contacts.xml"), new ContactHandler());
}
}
class ContactHandler extends DefaultHandler {
private String currentElement = "";
private String name = "";
private String email = "";
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) {
currentElement = qName;
if ("person".equals(qName)) {
String id = attributes.getValue("id");
System.out.println("새 사람, id: " + id);
}
}
@Override
public void characters(char[] ch, int start, int length) {
String text = new String(ch, start, length).trim();
if (text.isEmpty()) return;
if ("name".equals(currentElement)) {
name = text;
} else if ("email".equals(currentElement)) {
email = text;
}
}
@Override
public void endElement(String uri, String localName, String qName) {
if ("person".equals(qName)) {
System.out.println("이름: " + name + ", 이메일: " + email);
name = "";
email = "";
}
currentElement = "";
}
}
이 코드가 하는 일을 간단히 살펴봅시다. SAX 파서가 <person> 같은 시작 태그를 만나면 startElement 메서드를 호출합니다.そこで id 속성을 읽어 바로 출력합니다. 요소 안에서 텍스트(예: 이름이나 e-mail)를 만나면 characters 메서드로 제어가 넘어가고, 여기서 임시 변수에 텍스트를 저장합니다. 그리고 닫는 태그 </person>에 도달하면 endElement가 호출됩니다. 이 시점에는 사람의 이름과 e-mail을 알고 있으므로 출력할 수 있습니다. 이후 다음 연락처에 대비해 변수를 초기화합니다.
핵심은 SAX가 전체 XML을 메모리에 보관하지 않고 스트리밍 “리더”처럼 파일을 위에서 아래로 지나가며 이벤트(태그 시작, 텍스트, 태그 종료)에 반응한다는 점입니다. 특히 대용량 파일에서 빠르고 메모리 효율적입니다.
DOM과 SAX의 간단 비교
| DOM | SAX | |
|---|---|---|
| 스타일 | 트리(전체 구조를 메모리에 유지) | 이벤트(“온더플라이” 처리) |
| 메모리 | 전체 XML 로드 | 최소한의 메모리 사용 |
| 변경 | 읽기/수정/추가 가능 | 읽기 전용(일반적) |
| 데이터 검색 | 어디서든 쉽게 탐색 | 현재 위치를 직접 추적해야 함 |
| 파일 크기 | 소·중형 파일에 적합 | 초대용량 파일에 적합 |
3. DOM을 쓸 때와 SAX를 쓸 때
DOM은 다음과 같은 경우에 적합합니다:
- 파일이 작거나 중간 크기인 경우.
- XML의 여러 부분에 반복적으로 접근해야 하는 경우.
- 문서 구조를 수정해야 하는 경우.
SAX가 더 나은 선택인 경우:
- 파일이 너무 커서 메모리에 올릴 수 없는 경우.
- 일부 정보만 빠르게 “뽑아내야” 하는 경우(예: 특정 속성을 가진 <item> 요소를 모두 찾기).
- 높은 성능과 최소한의 메모리 사용이 필요한 경우.
- XML을 수정하지 않고 읽기만 할 계획인 경우.
팁:
실제 프로젝트에서는 두 접근법을 함께 쓰는 경우가 많습니다. DOM은 사람이 읽는 설정이나 작은 구성 파일에, SAX는 로그, 덤프, 대용량 가져오기에 사용합니다.
4. 실습: 애플리케이션을 위한 작은 파서
학습용 애플리케이션(예: “연락처”)에서, gmail.com 메일을 가진 사용자 수를 빠르게 집계해야 한다고 해 봅시다.
DOM 해법:
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new File("contacts.xml"));
NodeList emails = doc.getElementsByTagName("email");
int count = 0;
for (int i = 0; i < emails.getLength(); i++) {
String email = emails.item(i).getTextContent();
if (email.endsWith("@gmail.com")) {
count++;
}
}
System.out.println("gmail.com 사용자 수: " + count);
SAX 해법:
class GmailCounterHandler extends DefaultHandler {
private String currentElement = "";
int count = 0;
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) {
currentElement = qName;
}
@Override
public void characters(char[] ch, int start, int length) {
if ("email".equals(currentElement)) {
String email = new String(ch, start, length).trim();
if (email.endsWith("@gmail.com")) {
count++;
}
}
}
@Override
public void endDocument() {
System.out.println("gmail.com 사용자 수: " + count);
}
}
5. DOM과 SAX 사용 시 특징과 주의점
- DOM은 XML 파일이 매우 크면 메모리를 다 써버릴 수 있습니다. OutOfMemoryError가 나타난다면 SAX로 전환할 때일 가능성이 큽니다.
- SAX는 주의를 요합니다. 현재 어떤 요소에 있는지 추적하고, 필요한 데이터를 정확히 모아야 합니다. 깊게 중첩된 구조에서는 스택이나 보조 변수를 써서 혼동을 피해야 할 때가 있습니다.
- SAX에서 characters 메서드는 하나의 텍스트 블록에 대해 여러 번 호출될 수 있습니다(특히 텍스트가 길거나 특수 문자가 포함된 경우). StringBuilder에 텍스트를 누적하는 것이 좋습니다.
- DOM은 검색, 내비게이션, 구조 변경에는 탁월하지만 스트리밍 처리에는 맞지 않습니다.
- 어떤 접근법을 써야 할지 확신이 없다면 단순한 DOM부터 시작하고, 메모리가 “버거워지면” SAX로 갈아타세요.
6. DOM과 SAX 작업 시 흔한 오류
오류 №1: SAX에서 공백과 줄바꿈을 잘못 처리.
characters 메서드는 요소 사이의 공백과 줄바꿈을 포함한 텍스트 조각을 반환할 수 있습니다. .trim().isEmpty()를 걸러내지 않으면 “빈” 호출이 많아지거나 텍스트를 잘못 조합할 수 있습니다.
오류 №2: SAX로 XML을 변경하려는 시도.
SAX는 읽기 전용입니다! 구조를 변경하려면 DOM을 사용하세요.
오류 №3: SAX의 이벤트 순서 처리 오류.
endElement에서 변수를 초기화하지 않으면 요소 사이에 데이터가 “새는” 문제가 생길 수 있습니다.
오류 №4: 초대용량 파일에 DOM 사용.
결과는 OutOfMemoryError이거나 매우 느린 동작입니다.
오류 №5: DOM에서 잘못된 형변환.
DOM에서는 모든 것이 Node이지만, 속성과 자식 요소를 다루려면 Element로 캐스팅해야 합니다. 잘못 캐스팅하면 ClassCastException이 발생할 수 있습니다.
GO TO FULL VERSION