DTD: 콘텐츠 모델과 엔터티로 XML 구조를 선언하는 방식

DTD의 ELEMENT/ATTLIST/엔터티 선언, 내부·외부 서브셋, XXE 방지를 포함한 검증 파이프라인을 XSD·RELAX NG와 비교해 정리한다.

2026-08-15 · 최초 발행 2025-12-03

DTD는 산업 표준 명칭으로는 "Document Type Definition"이지만, 여기서는 XML 생태계에서 실질적으로 통용되는 XML DTD를 다룬다. 요소·속성·순서·중첩·발생 횟수 같은 문서의 허용 형태를 선언하는 언어로, XSD보다 먼저 자리 잡았고 지금도 레거시 표준 곳곳에 남아 있다.

무엇을 검증하고 무엇을 못 하는가

XML well-formed는 태그의 중첩과 종료 같은 문법적 정합성을 만족했는지를 뜻한다. DTD가 다루는 범위는 이 문법 적합 여부를 넘어선 구조적 유효성(validity)이다. 즉, 문법적으로 맞는 XML이라도 DTD가 선언한 요소·속성·콘텐츠 모델 규칙을 어기면 유효한 문서는 아니다.

하지만 그 표현력에는 뚜렷한 한계가 있다 — 데이터 타입은 CDATA·ID·IDREF·NMTOKEN·ENUM 등으로 제한되고, 사용자 정의 복합 타입이 없으며, 무엇보다 네임스페이스를 인식하지 못한다. 이 세 가지 한계가 결국 XSD·RELAX NG로 이어지는 이유다.

선언 단위: ELEMENT·ATTLIST·ENTITY·NOTATION

DTD 구조 정의는 ELEMENT 선언에서 시작한다. 시퀀스(,), 선택(|), 발생 제약(*, +, ?)으로 콘텐츠 모델을 표현하고, 혼합 콘텐츠(#PCDATA)나 빈 요소(EMPTY), 임의 콘텐츠(ANY)도 지정할 수 있다.

속성은 ATTLIST로 CDATA, ID/IDREF(S), NMTOKEN(S), ENTITY(IES), NOTATION, 열거형 타입을 지정한다. 기본값 규칙도 함께 명시하는데 #REQUIRED, #IMPLIED, #FIXED, 리터럴 기본값 네 종류다.

일반/매개변수 엔터티는 텍스트 조각을 재사용하는 메커니즘이다. 반면 표기 선언(NOTATION)은 이미지·바이너리처럼 비-XML 데이터 형식의 이름을 선언하고 비파싱 엔터티와 연결하는 별도 기능이다. PUBLIC/SYSTEM 식별자로 외부 자원을 참조하는데, 운영 시에는 XML Catalog로 오프라인·캐시 운영을 하는 편이 안전하다.

내부 서브셋 vs 외부 DTD

배치 형태는 두 가지다. 내부 서브셋은 문서 안에 DOCTYPE으로 직접 선언하는 방식이라 단일 파일로 배포하기는 쉽지만, 변경이 생기면 문서마다 손을 대야 해서 전파가 어렵다. 외부 DTD는 파일이나 식별자로 참조하는 방식이라 재사용성과 버전 관리가 쉬운 대신, 네트워크 의존성과 보안 문제를 함께 끌고 온다. 파서가 DOCTYPE을 인식해 DTD를 로드하고 구조를 검증하는데, 실제 도구로는 xmllint(libxml2), Xerces(JAXP), Python lxml 등이 쓰인다.

활용 사례

표준 콘텐츠 출판 체계에서는 XHTML 1.x, DocBook 같은 전통 규격을 DTD로 검증한다. 오프라인 퍼블리싱 파이프라인에서는 가볍게 쓸 수 있는 검증 수단으로 남아 있다.

레거시 B2B·기관 연계에서는 과거에 합의한 DTD 기반 메시지 포맷을 그대로 유지하며 변경을 최소화하는 전략을 쓴다. 외부 DTD를 사내에 미러링해두면 안정적으로 검증할 수 있다.

임베디드·에이전트 환경처럼 메모리·의존성을 최소화해야 하는 경량 설정·로그 포맷 검증에서도 DTD가 쓰인다. 간단한 제약 조건만 필요한 구간에서는 빠르게 유효성을 확인할 수 있다.

외부 DTD 기반 검증 파이프라인 구현

전제조건은 Linux/macOS/Windows에서 libxml2 2.9+(xmllint), Java 11+(JAXP 내장)이고, 보안상 외부 엔터티의 네트워크 접근을 차단하고 로컬 DTD 캐시·카탈로그를 쓰는 것이 원칙이다.

구현할 때는 먼저 루트 요소와 문서 트리, 필수·선택 요소와 반복 규칙을 정한 뒤 콘텐츠 모델·속성 제약·기본값·엔터티 선언을 DTD에 작성한다. 여러 문서에서 공유할 구조라면 외부 .dtd 파일로 분리하고, XML 문서의 DOCTYPE에서 그 DTD를 참조해 연결한다. 이후 검증 명령이나 빌드·CI 단계가 연결된 문서와 DTD를 함께 읽어 유효성을 확인한다.

<!-- book.dtd: 외부 DTD 예시 -->
<!ELEMENT catalog (book+)>
<!ELEMENT book (title, author+, price?, cover?)>
<!ATTLIST book
  isbn   ID                                  #REQUIRED
  status (draft|released|out-of-print)       "draft">
<!ELEMENT title  (#PCDATA)>
<!ELEMENT author (#PCDATA)>
<!ELEMENT price  (#PCDATA)>
<!ELEMENT cover  EMPTY>
<!ATTLIST cover
  image  ENTITY                               #REQUIRED
  format NOTATION (png|jpeg)                  #REQUIRED>
<!NOTATION png  SYSTEM "image/png">
<!NOTATION jpeg SYSTEM "image/jpeg">
<!ENTITY logo SYSTEM "logo.png" NDATA png>
<!-- catalog.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE catalog SYSTEM "book.dtd">
<catalog>
  <book isbn="b-001" status="released">
    <title>XML with DTD</title>
    <author>A. Kim</author>
    <author>B. Lee</author>
    <price>19.99</price>
    <cover image="logo" format="png"/>
  </book>
</catalog>
DocumentBuilderFactory f = DocumentBuilderFactory.newInstance();
f.setValidating(true);
f.setNamespaceAware(true);
f.setFeature(javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING, true);
// 외부 접근 범위 제한
f.setAttribute(javax.xml.XMLConstants.ACCESS_EXTERNAL_DTD, "file");
f.setAttribute(javax.xml.XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
DocumentBuilder b = f.newDocumentBuilder();
b.setEntityResolver((pub, sys) -> new InputSource(
  new java.io.StringReader(""))); // 필요 시 카탈로그 적용으로 대체
b.parse(new File("catalog.xml"));

샘플 DTD(catalog.dtd)는 다음과 같다.

<!ELEMENT catalog (book+)>
<!ELEMENT book (title, price)>
<!ATTLIST book
  id ID #REQUIRED
  category (tech|novel) #IMPLIED>
<!ELEMENT title (#PCDATA)>
<!ELEMENT price (#PCDATA)>

이 DTD를 참조하는 샘플 XML(catalog.xml)이다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE catalog SYSTEM "catalog.dtd">
<catalog>
  <book id="b1" category="tech">
    <title>XML in Action</title>
    <price>29.99</price>
  </book>
</catalog>

커맨드라인 검증은 xmllint로 한 줄이면 된다. 검증 결과와 오류는 이 명령의 실행 경로를 통해 반환되고, CI에서는 생성된 에러 리포트가 실패 시 빌드를 막는 경로로 이어진다.

# DTD를 DOCTYPE으로 참조하는 문서 유효성 검증
xmllint --noout --valid catalog.xml

Java(JAXP)로 검증할 때는 외부 엔터티의 네트워크 접근을 명시적으로 차단하고 로컬 DTD로만 해석하도록 EntityResolver를 지정하는 편이 안전하다. 이 경로에서는 오류와 경고가 ErrorHandler를 통해 표준 오류로 반환되고, 정상 검증 결과는 Validation OK로 출력된다.

// Java 11+, 표준 JAXP
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;
import javax.xml.XMLConstants;
import javax.xml.parsers.*;
import java.io.*;

public class DtdValidate {
  public static void main(String[] args) throws Exception {
    if (args.length != 2) {
      System.err.println("Usage: java DtdValidate <xml-file> <local-dtd-path>");
      System.exit(1);
    }
    String xmlPath = args[0];
    String localDtd = args[1];

    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    dbf.setValidating(true);
    dbf.setNamespaceAware(false);
    dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
    // 외부 엔터티 네트워크 접근 차단
    try {
      dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
      dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
    } catch (ParserConfigurationException ignored) {}

    DocumentBuilder db = dbf.newDocumentBuilder();
    // 로컬 DTD로만 해석하도록 EntityResolver 설정
    db.setEntityResolver((publicId, systemId) -> new InputSource(new FileReader(localDtd)));
    db.setErrorHandler(new DefaultHandler() {
      @Override public void error(SAXParseException e) { System.err.println("ERROR: " + e.getMessage()); }
      @Override public void fatalError(SAXParseException e) { System.err.println("FATAL: " + e.getMessage()); }
      @Override public void warning(SAXParseException e) { System.err.println("WARN: " + e.getMessage()); }
    });

    db.parse(new File(xmlPath));
    System.out.println("Validation OK");
  }
}

CI에 연동할 때는 입력 → 처리 → 출력 흐름으로 잡으면 된다. PR에 포함된 XML 파일 목록을 수집하고, xmllint나 JAXP Validator로 병렬 검증하되 외부 접근은 차단하며, 에러 리포트를 생성해 실패 시 빌드를 막는다. XML Catalog나 로컬 DTD 경로 매핑을 캐시 전략으로 쓰면 검증이 빨라진다.

다이어그램: DTD 검증 흐름

오류 있음오류 없음XML 입력파서 초기화 보안 설정('XXE차단')DTD 로드('내부 서브셋' 또는'외부 DTD')검증 결과 평가('오류 존재여부')에러 리포트 실패 처리('로그,빌드 중단')DOM/SAX 트리 생성애플리케이션 로직 실행

DTD vs XSD vs RELAX NG

지표 DTD XML Schema (XSD) RELAX NG
성능 파서 내장 구현과 문법 단순성으로 빠른 검증 경향 타입 검증 복잡성으로 상대적 오버헤드 설계 간결성으로 빠름, 구현체에 따라 상이
확장성 사용자 정의 타입 부재, 네임스페이스 비지원 한계 풍부한 타입/네임스페이스/파셜 재사용 우수 표현력 높음, XML/Compact 두 문법 지원
일관성 단순 규칙으로 운영 일관성 양호 스키마 버전/네임스페이스 관리 필요 스키마 조합 유연, 팀 표준화 필요
안정성 성숙도 높음, 외부 엔터티 보안 이슈 주의 성숙/광범위 사용, 복잡성 증가 품질 높은 구현 다수, 도입 조직 경험 중요
운영 편의 작성·리뷰 용이, 제약 표현력 제한 도구 생태계 풍부, 학습·유지 난이도 높음 가독성 우수, 툴링 채택 조직별 편차

도입 효과와 남는 리스크

구조 오류를 사전에 차단하고 ID/IDREF 참조 무결성을 확보하면 빌드 단계에서 걸러지는 만큼 런타임 오류 발생률이 줄어든다. DTD 규칙 위반은 검증 통합 시 100% 탐지할 수 있다. 외부 DTD를 로컬 캐시·카탈로그로 돌리면 네트워크 의존을 없애 검증 시간이 단축되는데, 레거시 환경을 기준으로 검증 지연을 80% 이상 축소할 수 있다(정량 예시, 보수적 가정). CI에 사전 검증을 도입하면 배포 전 스키마 위반 누락률을 30~50% 줄일 수 있다(정량 예시, 보수적 가정). 문서 스펙의 단일 진실 소스를 DTD 하나로 못박아두면 XHTML 1.x, DocBook 같은 레거시 표준과의 호환도 유지하기 쉬워진다.

다만 이 모든 이점은 경량 검증이 필요한 범위 안에서다. 사용자 정의 타입이나 네임스페이스가 필요해지는 순간 DTD는 한계에 부딪히므로, 그 시점에는 XSD나 RELAX NG로 넘어가는 게 맞다. 외부 엔터티 차단, 로컬 카탈로그·리졸버 사용, CI 단계 자동 검증은 DTD를 계속 쓰기로 했을 때 최소한으로 갖춰야 할 운영 모범사례다.

DTDXML검증엔터티XXE레거시표준