ADR-003: Multi-document redesign β xlwings-inspired App/Docs/Document
μ΄ ADR μ μ±ν λμ΄ v3.0 μΌλ‘ ꡬνλ¨. - λ²μ : v3.0 (semver μ μ§, clean break) - Document.activate(): λͺ¨λ doc-scoped λ©μλ μμ μ μλ νΈμΆ - v2 νΈν shim: μμ β App.open/save/save_as/close/doc μ¦μ μ κ±° - μ¬μ©μ λ§μ΄κ·Έλ μ΄μ
: v2βv3 κ°μ΄λ
λ§₯λ½ (Context)
v2.0 μ μ¬λ¦Ό facade λ ν λ²μ ν λ¬Έμλ§ λ€λ£¨λ λͺ¨λΈλ‘ μΆλ°νμ΅λλ€. App μ΄ lifecycle κ³Ό λ¨μΌ νμ± λ¬Έμ (app.doc) λ₯Ό λͺ¨λ μ±
μμ§κ³ , λλΆλΆμ μμ
μ΄ app.open() / app.save() / app.close() μ κ°μ΄ App λ©μλ λ‘ λ
ΈμΆλ©λλ€. μ΄ λͺ¨λΈμ λ€μκ³Ό κ°μ λ³Έμ§μ νκ³λ₯Ό λλ¬λμ΅λλ€:
λ€μ€ λ¬Έμ λΆμ¬. HWP λ ν νλ‘μΈμ€μ μ¬λ¬ λ¬Έμλ₯Ό λμμ μ΄ μ μμ§λ§ (
XHwpDocuments), v2 surface λ κ·Έ collection μ λ ΈμΆνμ§ μμ΅λλ€.app.documentsλ v1 μμ μ¬μ©λμμΌλ v2 redesign μ βPhase 3 μΌλ‘ μ°κΈ°β λ‘ νμλλ©° μ κ±°λμμ΅λλ€ (migration guide μ°Έμ‘°). κ²°κ³Όμ μΌλ‘ μ¬μ©μκ° ν λ²μ λ νμΌμ λΉκ΅/λ³ν©/λ³΅μ¬ νλ €λ©΄ raw COM (app.api.XHwpDocuments) μΌλ‘ λ΄λ €κ°μΌ ν©λλ€.lifecycle μ± μμ μλͺ»λ μμΉ.
app.save()λ βμ΄λ λ¬Έμλ₯Ό?β κ° λͺ¨νΈν©λλ€ β νμ¬λ νμ± λ¬Έμλ₯Ό μ μ₯νμ§λ§, μ¬μ©μκ° λ¬Έμ A λ₯Ό μ²λ¦¬νλ€ background μμ λ¬Έμ B κ° νμ±νλλ©΄ μλμ λ€λ₯Έ λ¬Έμκ° μ μ₯λ©λλ€. μ μ₯/λ«κΈ°λ λ¬Έμ λ¨μ μμ μ΄ μμ°μ€λ½μ΅ λλ€.Document λ©μλκ° λΉμ΄ μμ.
app.docμ 7κ° collection (fields/bookmarks/hyperlinks/images/paragraphs/styles/tables) λ§ κ°μ§κ³ μκ³ , migration κ°μ΄λκ° μ½μνλinsert_text/text/cursor/find_text/replace_allλ±μ μ‘΄μ¬νμ§ μμ΅λλ€. μ¬μ©μκ° λ¬Έμλλ‘ λ°λΌ μ½λ©νλ©΄ μ¦μAttributeError. μ΄λ λ¨μ λλ½μ΄ μλλΌ Document μ μ± μ λ²μκ° μ ν΄μ§μ§ μμ κ²°κ³Ό μ λλ€ β App μμ λΉΌκΈ΄ νμ§λ§ μ΄λμλ λμ§ μμ.ν μ€νΈ 격리 μ΄λ €μ. v2 μ
App()μ resident HWP μΈμ€ν΄μ€ μ attach ν©λλ€ β λ¬Έμ 1κ°λ§ μ΄λ¦¬λ λͺ¨λΈμ΄λΌ ν μ€νΈ μ¬μ΄μ λ¬Έμ λμ μ΄ μ μ 보μμ§λ§, λ€μ€ λ¬Έμ μν¬νλ‘μ° μΆκ° μ βμ΄λ λ¬Έμκ° νμ±?β μ race κ° μ¦μ λλ¬λ κ²μ λλ€.
μκ° (Inspiration) β xlwings
xlwings λ Excel μλνμ λμΌν deep COM μλν λ¬Έμ λ₯Ό νμκ³ , λ€μκ³Ό κ°μ 3-tier ꡬ쑰λ₯Ό μ μ°©μμΌ°μ΅λλ€:
xw.App # Excel application instance
βββ books # Books collection (workbook λ€)
β βββ open(path) β Book # νμΌ μ΄κΈ° β Book λ°ν
β βββ add() β Book # μ λΉ workbook
β βββ active β Book # νμ¬ νμ± workbook
β βββ [i] / [name] # index λλ μ΄λ¦ μ κ·Ό
β βββ for wb in books # μν κ°λ₯
βββ apps # μ¬μ΄λΈλ§ β λ€λ₯Έ Excel νλ‘μΈμ€
βββ quit() # Excel μ체 μ’
λ£
Book (= κ°λ³ workbook μΈμ€ν΄μ€)
βββ name / fullname / path
βββ sheets # Sheet collection (per-Book)
βββ save() / save_as(path) # λ¬Έμ λ¨μ μ μ₯
βββ close(save=False) # λ¬Έμ λ¨μ λ«κΈ°
βββ activate() # μ΄ workbook μ νμ±ν
βββ selection # νμ¬ μ ν
ν΅μ¬ μμΉ:
App= νλ‘μΈμ€ lifecycle λ§ (start, quit). λ¬Έμ μ체μ λν λ©μλ μμ.App.books= workbook collection. μ΄ collection μ λ©μλ (open,add) κ° Book μΈμ€ν΄μ€λ₯Ό λ°ν νκ³ , μ¬μ©μλ κ·Έ μΈμ€ν΄μ€λ₯Ό λ³μλ‘ μ‘μμ μμ ν©λλ€.Book= ν workbook μ λͺ¨λ μ± μ (μ μ₯, λ«κΈ°, sheet μ κ·Ό, μ μ‘°μ). κ°μ App μμ μ¬λ¬ Book μ΄ λμμ μμ μ μκ³ μλ‘ λ 립μ .
κ²°μ (Decision)
hwpapi v3 (λλ v2.1) μ xlwings μ 3-tier ꡬ쑰λ₯Ό μ°¨μ©ν©λλ€.
hwpapi.App # HWP COM μμ§ lifecycle λ§
βββ docs # Documents collection
β βββ open(path) β Document
β βββ add() β Document # μ λΉ λ¬Έμ
β βββ active β Document # νμ¬ νμ± λ¬Έμ
β βββ [i] / [name]
β βββ for d in docs
βββ visible (property)
βββ version (property)
βββ engine # raw escape hatch
βββ quit() # HWP νλ‘μΈμ€ μ’
λ£
hwpapi.Document # ν .hwp νμΌμ λν λͺ¨λ μ±
μ
βββ name / path / saved # λ©ν (μμ μ¬λΆ ν¬ν¨)
βββ text (r/w property)
βββ insert_text(...)
βββ insert_picture(...) / insert_table(...) / ...
βββ find_text(...) / replace_all(...)
βββ select_all() / get_selected_text()
βββ fields / bookmarks / hyperlinks / images /
β paragraphs / styles / tables / controls
βββ cursor # per-Document 컀μ
βββ page # per-Document νμ΄μ§ μ€μ
βββ save() / save_as(path) # λ¬Έμ λ¨μ μ μ₯
βββ close(save=False) # λ¬Έμ λ¨μ λ«κΈ° β App μν₯ μμ
βββ activate() # μ΄ λ¬Έμλ₯Ό HWP μ°½μμ νμ±ν
βββ api / engine # per-doc μ COM νΈλ€ (XHwpDocument)
Process-level μμ binding μ μ± (Actions / ParameterSets / API)
HWP μ raw HAction, HParameterSet, XHwpDocuments κ°μ μμμ νλ‘μΈμ€ μ μ μ΄κ³ νμ νμ± λ¬Έμ μ μλν©λλ€. λ€μ€ λ¬Έμ μν©μμ μ΄λ doc μ λͺ
λ Ήμ΄ κ°μ§ λͺ¨νΈν΄μ§λ―λ‘, λ€μ μ μ±
μ μ ν©λλ€:
κ°μ μμμ λ μμΉμ λ ΈμΆ
| μμ | App λ 벨 (raw) | Document λ 벨 (doc-scoped) |
|---|---|---|
actions |
app.actions.X |
doc.actions.X β run() μ§μ doc.activate() μλ |
parametersets |
app.engine.parametersets (raw ν΄λμ€) |
doc.create_parameterset(...) β auto-activate |
api (COM νΈλ€) |
app.api β IHwpObject (process) |
doc.api β IXHwpDocument (per-doc) |
engine |
app.engine β Engine κ°μ²΄ |
(κ·Έλλ‘ β process μμ) |
binding μλ―Έ
| μ§μ μ | νμ±ν μ± μ | μΆμ² μ¬μ©μ² |
|---|---|---|
app.actions.X |
μ¬μ©μ (doc.activate() μλ) |
escape hatch / λ¨μΌ doc |
doc.actions.X |
μλ (run μμ ) | μΌλ° μ¬μ© / λ€μ€ doc |
ꡬν β μμ proxy
class _DocActions:
"""Document-scoped actions proxy."""
def __init__(self, doc):
self._doc = doc
self._app = doc._app
def __getattr__(self, name):
action = getattr(self._app.actions, name)
return _ScopedAction(action, self._doc)
class _ScopedAction:
def __init__(self, action, doc):
self._action = action
self._doc = doc
@property
def pset(self):
return self._action.pset
def run(self, *a, **kw):
self._doc.activate() # ν΅μ¬
return self._action.run(*a, **kw)
class Document:
@property
def actions(self):
return _DocActions(self)μ¬μ©μ λ©ν λͺ¨λΈ
doc.actions.X.run() β "doc μ X μ μ©" (μμ )
app.actions.X.run() β "νμ¬ νμ± doc μ X μ μ© (μ¬μ©μ μ±
μ)"
xlwings μ book.api vs app.api μ μλ―Έ λμΌ.
μ€λ²ν€λ
Document.activate() = COM νΈμΆ 1 ν (~Β΅s). λ¨μΌ doc μΌ λ noop (μ΄λ―Έ νμ±). λ€μ€ doc μΌ λλ§ μ€μ§ λΉμ© λ°μ, μ΄λ§μ λ μ¬μ©μκ° μ°μν΄μ κ°μ doc μ λ€λ£¨λ κ²½μ° μΊμ±μΌλ‘ 0 μΌλ‘ μΆμ κ°λ₯.
μ¬μ© μ (Examples)
λ¨μΌ λ¬Έμ β v2 μ κ±°μ λμΌ
from hwpapi import App
with App() as app:
doc = app.docs.open("report.hwp")
doc.insert_text("μ μ€\n")
doc.save()
doc.close()λ λ¬Έμλ₯Ό λμμ λ€λ£¨κΈ° (νμ¬ λΆκ°λ₯νλ κ²)
from hwpapi import App
with App() as app:
src = app.docs.open("source.hwp")
dst = app.docs.add() # λΉ μ λ¬Έμ
for para in src.paragraphs:
dst.insert_text(para.text + "\n")
dst.save_as("merged.hwp")
src.close(save=False) # μλ³Έμ κ·Έλλ‘
dst.close()μΌκ΄ λ³ν β ν App μμ Nκ° νμΌ
from hwpapi import App
from pathlib import Path
with App() as app:
for hwp in Path("inbox").glob("*.hwp"):
doc = app.docs.open(hwp)
doc.save_as(hwp.with_suffix(".pdf"))
doc.close() # λ©λͺ¨λ¦¬ ν΄μ , λ€μ νμΌλ‘νμ± λ¬Έμ μ ν
docs = app.docs # collection
print(len(docs)) # μ΄λ¦° λ¬Έμ κ°μ
for d in docs:
print(d.name)
docs[0].activate() # 첫 λ¬Έμλ₯Ό forefront λ‘λ§μ΄κ·Έλ μ΄μ λ¨κ³ (Phases)
Phase 0 β λͺ μΈ λκ²°
μ΄ ADR μ±ν ν v2.x λΌμΈμ frozen. v2 μ¬μ©μλ hwpapi==2.* pin. v3 (λλ v2.1) μ μ minor λ‘ μΆμ.
Phase 1 β Documents collection μ μ€
hwpapi.collections.documents.DocumentCollectionμμ±- App μ
app.docsproperty μΆκ°,app.docμapp.docs.activeλ‘ λ΄λΆ alias (v2.x νΈν μμ). app.docs.open(path)κ°DocumentμΈμ€ν΄μ€λ₯Ό λ°ν (νμ¬λ Document κ° λΉ facade λΌλ collection λ§ μΌλ¨ λ ΈμΆ).
Phase 2 β Document μ λ©μλ μ΄μ
App.open/save/save_as/closeβDocument.open/save/save_as/closeλ‘ μ΄λ. App λ©μλλ μΌλ¨ deprecation shim μΌλ‘ λ¨κ²¨ active doc μΌλ‘ μμ (app.save()βapp.docs.active.save()).- text I/O β
insert_text,find_text,replace_all,select_all,get_selected_text,text(property),clearλ₯Ό Document μ μ μ€. λ΄λΆ ꡬνμ rawactions.InsertTextλ±μ μ¬μ©. - cursor β
Document.cursor.goto_page(...),move_to_field,in_tableλ±. - 7 κ° collection accessor λ κ·Έλλ‘ (μ΄λ―Έ Document μ μμ).
Phase 3 β App μ lifecycle μ 리
- App μ λ¨λ λ©μλ:
quit,visible,version,docs,engine,api. κ·Έ μΈλ deprecated β v3.1 μμ μ κ±°. App() __enter__/__exit__κ°quit()λ§ μ± μ. μ΄λ¦° λ¬Έμλ μλ λ«μ§ μμ (μ¬μ©μκ° λͺ μμ μΌλ‘ close λλ with-Document μ¬μ©).
Phase 4 β Document μ context manager
with app.docs.open("a.hwp") as doc:
doc.insert_text("...")
# exit μ μλ close (μ μ₯ μ¬λΆλ docs.open(..., save_on_close=) λ‘ μ μ΄)Phase 5 β νΈν shim μ κ±°
app.open/save/close/doc λ± v2 νΈν alias λ₯Ό μ κ±°νκ³ v3.0 stable release. v2 μ¬μ©μλ migration κ°μ΄λ (μ΄ ADR μ λΆλ‘) λ°λΌ λ§μ΄κ·Έλ μ΄μ
.
v2 β v3 λ³ν ν (μμ½)
| v2 | v3 |
|---|---|
app.open(path) |
doc = app.docs.open(path) |
app.new() |
doc = app.docs.add() |
app.save() |
doc.save() |
app.save_as(path) |
doc.save_as(path) |
app.close() |
doc.close() |
app.doc |
app.docs.active |
| (μμ β multi-doc) | for d in app.docs: |
| (μμ β λ€μ€ λ¬Έμκ° λΉκ΅) | app.docs["a.hwp"], app.docs[0] |
νΈνμ± μ μ± (Compatibility)
- v2.x: λκ²° (bug fix λ§)
- v3.0: deprecation shim ν¬ν¨, v2 μ½λ λλΆλΆ μλ
- v3.1: shim μ κ±°, clean cut
v2 β v3 λ§μ΄κ·Έλ μ΄μ
μ v1 β v2 λ³΄λ€ ν¨μ¬ μμ β μ£Όλ‘ app.X λ₯Ό doc = app.docs.open(...) + doc.X λ‘ λ°κΎΈλ κ². find/replace κ°λ₯ν ν¨ν΄.
κ²°κ³Ό (Consequences)
κΈμ
- λ€μ€ λ¬Έμ μν¬νλ‘μ° κ°λ₯ β μΌκ΄ λ³ν, λ³ν©, λΉκ΅ λ±
- μ μ₯/λ«κΈ° μλ λͺ
ν β
doc.save()λ βμ΄ λ¬Έμβ κ° λͺ ν - Document μ± μ λ²μ λͺ ν β 7 collection + text I/O + cursor + save/close. App μ process lifecycle λ§.
- xlwings μ¬μ©μ μΉμ β Python λ°μ΄ν° λΆμκ°μκ² mental model λμΌ
- ν μ€νΈ 격리 κΉλ β κ° ν μ€νΈκ° μκΈ° Document λ₯Ό λͺ μμ μΌλ‘ μμ , race μμ
λΆμ / μν
- API νλ©΄μ΄ μ»€μ§ β Document κ° v2 μ κ±°μ λͺ¨λ App λ©μλλ₯Ό ν‘μ. μ¬λ¦Ό facade μ μ μ μΌλΆ νν΄ (λ€λ§ μ¬λ¦Όν κ²μ App λ§).
- win32com μ active doc μΆμ κ³Όμ μΆ©λ κ°λ₯μ± β HWP COM μ λͺ
μμ μΈ doc νΈλ€ μ λ¬μ΄ μΌκ΄λμ§ μμ (
HAction.Runμ νμ νμ± λ¬Έμ λμ). μ΄ λΆλΆμDocument.activate()λ₯Ό λͺ¨λ doc-scoped λ©μ λ μμ μ μλ νΈμΆνλ λ°©μμΌλ‘ μ°ν (xlwings λ λμΌ ν¨ν΄). - λ§μ΄κ·Έλ μ΄μ λΉμ© β v2 μ¬μ©μμκ² λ λ²μ§Έ breaking change (v1βv2βv3). μ΄ λΉμ©μ μ€μ΄λ €κ³ v3.0 μ deprecation shim ν¬ν¨.
- HWP μ λ€μ€ λ¬Έμ μ§μμ΄ μμ νμ§ μμ μ μμ β XHwpDocuments μ체λ μμ§λ§ μΌλΆ action μ νμ± λ¬Έμμλ§ μ μ©λ¨.
Document.apiλ₯Ό ν΅ν per-doc COM νΈμΆμ΄ κ°λ₯νμ§ κ²μ¦ νμ (Phase 1 spike).
κ²°μ (μ΄μ λ―Έν΄κ²° μ§λ¬Έ)
- β λ²μ β v3.0 (semver μ μ§, clean break)
- β
Document.activate()μλ νΈμΆ β λͺ¨λ doc-scoped λ©μλ μ§μ μ μλ (_DocActions.__getattr__μμ μλ μλ). xlwings μ λμΌ ν¨ν΄. COM νΈμΆ 1ν/λ©μλ β λ¨μΌ doc μΌ λ noop λΉμ©. - β
app.docs.add()β μ λΉ λ¬Έμλ₯Ό λ§λ€κ³ active λ‘ μ€μ ν ν΄λΉDocumentλ°ν. - β
app.docshim μ κ±° μμ β μ¦μ (v3.0). μ¬μ©μ λͺ μ κ²°μ β 무μμ΄ doc κ°μ²΄λ‘ κ°λμ§ λͺ ννλ―λ‘ deprecation μ¬μ΄ν΄ λΆνμ.
μ°Έκ³
- xlwings API reference
- HWP XHwpDocuments COM μΈν°νμ΄μ€
- ADR-001: v2 λ μΈ΅ API κ²°μ β μ΄ ADR μ ν λκ° λ λ¨μΌ-doc λͺ¨λΈ
- v2 migration κ°μ΄λ: Migrating from v1 to v2
- (μμ ) v3 migration κ°μ΄λ:
getting-started/migration-v2-to-v3.qmd