시작은 단순한 호기심에서

“한글 파일을 .NET에서 직접 다룰 수 있으면 좋겠는데…”

이런 생각을 한 .NET 개발자가 저만은 아닐 것입니다. HWP 파일은 한국에서 공공기관을 중심으로 여전히 널리 사용되는 문서 형식이지만, .NET 생태계에서는 이를 제대로 다룰 수 있는 오픈소스 라이브러리가 마땅치 않았습니다.

.NET에서는 그동안 HWP 파일을 다루기 위해서, Windows OS 한정으로 아래아한글을 설치하면 같이 따라오는 HWP ActiveX 컨트롤의 COM 타입 라이브러리를 불러와서 제어하는 것 정도만 겨우 다루어지고 있었을 뿐이었지만, 안타깝게도 이 마저도 지원이 중단되면서 지금은 길이 막힌 상태죠!

그러던 중 Java로 작성된 hwplib을 발견했습니다. neolord0님께서 꾸준히 유지보수해 온 이 라이브러리는 HWP 파일의 읽기/쓰기를 완성도 있게 지원하고 있었습니다.

예전 같았으면 이런 라이브러리를 포팅하는 것은 정말 투철한 사명감과 목적 의식이 있는게 아니라면 좀처럼 엄두를 낼 수 있는 작업이 아니었습니다. 하지만, 수준높은 AI 모델이 많이 나왔고 지금이라면 새로운 도전을 해 볼 수 있을거란 호기심이 있었습니다.

그렇게 시작된 3주간의 여정을 공유합니다.

숫자로 보는 프로젝트

본격적인 이야기에 앞서, Java 버전의 프로젝트의 규모를 숫자로 먼저 정리해 보겠습니다.

항목내용
원본 프로젝트neolord0/hwplib (Java)
포팅 프로젝트rkttu/libhwpsharp (C#)
타겟 프레임워크.NET Standard 2.0, .NET Framework 4.7.2, .NET 8
총 커밋 수54개
개발 기간2025-12-16 ~ 2026-01-08 (약 3주)
최초 포팅 코드641개 파일, 50,010 라인

641개 파일, 5만 줄. 솔직히 처음 이 숫자를 봤을 때는 “이게 가능할까?“라는 의문이 들었습니다.

첫날: AI와 함께한 대규모 코드 변환

5만 줄을 하루 만에?

2025년 12월 16일, 프로젝트를 시작했습니다. 일반적인 상황이라면 5만 줄의 Java 코드를 C#으로 변환하는 데 몇 달은 걸렸을 것입니다. 하지만 AI 코딩 어시스턴트가 게임 체인저였습니다.

AI에게 Java 파일들을 넘기고 C#으로 변환해달라고 요청했습니다. 물론 기계적인 변환만으로는 부족했습니다. Java와 C#은 비슷해 보이지만 미묘한 차이가 많기 때문입니다.

// Java
public byte getValue() { return value; }
public void setValue(byte value) { this.value = value; }
// C#
public byte Value { get; set; }

이런 패턴 변환은 AI가 잘 처리해줬습니다. 하지만 진짜 문제는 따로 있었습니다.

최대 난관: Apache POI에서 OpenMcdf로

OLE2 복합 문서의 늪

HWP 파일은 Microsoft의 OLE2 복합 문서 형식(Compound File Binary Format)을 기반으로 합니다. 쉽게 말해, 하나의 파일 안에 여러 개의 “스트림"이 폴더 구조처럼 저장되어 있는 형식입니다.

Java에서는 Apache POI 라이브러리가 이 형식을 다루지만, .NET에는 직접적인 대응물이 없습니다. 대신 OpenMcdf라는 라이브러리를 사용해야 했는데, 안타깝게도 Apache POI와는 API가 완전히 다릅니다.

// Java (Apache POI)
DirectoryEntry root = poiFs.getRoot();
DocumentInputStream stream = root.createDocumentInputStream("Section0");
// C# (OpenMcdf)
CFStorage root = compoundFile.RootStorage;
CFStream stream = root.GetStream("Section0");
byte[] data = stream.GetData();

개념은 비슷하지만 세부 구현이 완전히 달랐습니다. 이런 차이점을 이해하고 분석하면서 다듬는 과정을 과연 AI가 잘 해낼 수 있을지 솔직히 불안한 점이 있었습니다.

HWP 파일 내부의 데이터는 zlib으로 압축되어 있습니다. Java와 C#의 압축 해제 라이브러리가 미묘하게 다르게 동작해서, 같은 데이터를 넣어도 결과가 달랐습니다.

몇 시간 동안 바이트 단위로 비교하며 디버깅한 끝에, 문제의 원인을 찾았습니다. zlib 헤더 처리 방식이 달랐던 것입니다. 다행히 neolord0 님께서 꼼꼼하게 만들어주신 유닛 테스트 덕분에 이런 문제를 발견할 수 있었는데, 여기서 저는 Visual Studio 2026의 “에이전트 기반 디버거"의 힘을 느낄 수 있었습니다.

Visual Studio 2026은 많이들 알고 계시듯 그 어떤 버전보다도 AI 에이전트의 강력함을 누릴 수 있게 리팩토링된 제품입니다. 흔히 VS Code처럼 에이전트를 이용해서 코드를 “자동 작성"하는 것에 집중하기 마련인데, 이번 포팅 프로젝트에서 유닛 테스트를 디버그 모드로 진행하면서 “실패"하는 상황이 발생하면, 디버거를 열어둔 채로 “무엇이 문제인지” 즉시 파악하고, 스스로 중단점을 잡고 추적해가며 변수, 호출 스택을 내부 MCP 서버를 통해 수집해가며 대응하는 모습은 무척 흥미로웠습니다.

물론 모든 문제를 이렇게 풀 수 있었던 것은 아니었지만, 이런 도구의 지원이 없었다면 끝을 볼 수 없었을 수도 있는 일이었습니다.

테스트 100% 달성의 희열

빨간불에서 초록불로

원본 hwplib 프로젝트에는 neolord0 님께서 만들어둔 테스트 케이스들이 있었습니다. 저에게는 이 테스트들이 등대 같은 존재였습니다. “일단 이것만 다 통과하면 된다"는 명확한 목표가 있으니까요.

처음 테스트를 돌렸을 때는 참담했습니다. 47개 중 3개만 통과. 빨간 X 표시가 화면을 가득 채웠죠. 하지만 포기할 수 없었습니다. 하나씩 실패 원인을 추적하고, 코드를 수정하고, 다시 테스트를 돌렸습니다.

Section 파싱을 고치니 5개가 더 통과했습니다. 컨트롤 파싱을 수정하니 10개가 초록불로 바뀌었습니다. 테이블 처리, GSO(그래픽) 객체… 마치 퍼즐 조각을 맞추듯 하나씩 해결해 나갔습니다.

그리고 드디어, 그 순간이 왔습니다.

Tests Passed: 47/47

모니터를 뚫어져라 쳐다봤습니다. 진짜 47/47인지 몇 번이고 확인했습니다. 커피 한 잔 마시고 다시 돌려봤습니다. 여전히 47/47. 이제야 숨을 쉴 수 있었습니다.

이 순간을 위해 달려왔구나 싶었습니다.

더 많은 사용자를 위한 여정

“macOS에서는 안 되나요?”

테스트를 통과하고 기뻐하는 것도 잠시, 현실이 찾아왔습니다. 첫 번째 질문은 예상대로였습니다.

“혹시 Mac에서도 쓸 수 있나요?”

아차 싶었습니다. 저는 처음에 이미지 처리에 System.Drawing을 사용했는데, 이건 Windows 전용이나 다름없습니다. .NET Core 이후로 크로스 플랫폼이 대세가 되었는데, Windows에서만 돌아가는 라이브러리라니… 의미가 반감되는 느낌이었죠.

고민 끝에 SkiaSharp으로 전환하기로 했습니다. Google의 Skia 그래픽 엔진을 .NET으로 래핑한 라이브러리인데, Windows, macOS, Linux 모두에서 동일하게 동작합니다. 이미지 관련 코드를 전부 뜯어고치는 건 귀찮았지만, 더 많은 개발자가 쓸 수 있다면 그만한 가치가 있었습니다.

“저희는 아직 .NET Framework 4.7.2인데요…”

두 번째 도전은 조금 더 고통스러웠습니다. 현실 세계의 기업 환경은 생각보다 보수적입니다. .NET 8이 아무리 좋아도, 레거시 시스템과의 호환성 때문에 .NET Framework를 벗어나지 못하는 곳이 많습니다.

문제는… 제가 신나서 C# 12의 최신 문법을 마음껏 사용했다는 것입니다.

// C# 12 - 파일 범위 네임스페이스 (한 줄로 끝!)
namespace HwpLib.Reader;

public class HwpReader { }

이걸 .NET Framework에서 쓰려면 구식 문법으로 바꿔야 합니다.

// C# 8 호환 - 전통적인 네임스페이스 (중괄호 필수)
namespace HwpLib.Reader
{
    public class HwpReader { }
}

“에이, 그냥 중괄호 추가하면 되는 거 아니야?” 라고 생각할 수 있습니다. 맞습니다. 근데 그게 582개 파일입니다.

결과적으로 51,480줄 추가, 49,701줄 삭제라는 어마어마한 diff가 발생했습니다. GitHub에서 해당 커밋을 열면 브라우저가 잠시 멈출 정도였죠. C# 언어 버전도 12.0에서 8.0까지 단계적으로 내려야 했습니다. required 키워드, init 접근자, 파일 범위 네임스페이스… 편하게 쓰던 기능들을 하나씩 포기하며 과거로 돌아가는 기분이었습니다.

그래도 이 고통 덕분에 이제 .NET Framework 4.7.2부터 .NET 8까지, 거의 모든 .NET 환경에서 libhwpsharp를 사용할 수 있게 되었습니다.

AI와의 협업에서 배운 것

3주간의 프로젝트를 마치고 나니, AI와 어떻게 협업해야 하는지에 대한 나름의 노하우가 생겼습니다.

AI가 빛나는 순간들

가장 인상적이었던 건 반복 작업에서의 효율성이었습니다. Java 파일 수백 개를 C#으로 변환하는 건 사람이 하면 며칠이 걸리는 단순 노동입니다. 하지만 AI에게는 그냥 “이거 C#으로 바꿔줘"라고 하면 끝이었습니다.

XML 문서 주석을 추가하는 작업도 마찬가지였습니다. 111개 파일에 2,000줄 이상의 API 문서를 추가해야 했는데, AI에게 “이 클래스가 뭐하는 건지 주석으로 설명해줘"라고 하면 맥락을 파악해서 적절한 설명을 생성해줬습니다.

하지만 이번 프로젝트에서 진짜 게임 체인저는 Visual Studio 2026의 에이전트 기반 디버거였습니다.

솔직히 말하면, AI 코딩 어시스턴트는 이제 어디서나 볼 수 있습니다. VS Code에도 있고, JetBrains IDE에도 있고, 심지어 웹 브라우저에서도 쓸 수 있죠. 코드 자동 완성이나 생성 기능은 이미 커모디티가 되었습니다. 그런데 Visual Studio 2026은 다른 곳에서 차별화를 만들었습니다.

유닛 테스트를 디버그 모드로 돌리다가 실패가 발생하면, 에이전트가 알아서 움직이기 시작합니다. “뭐가 문제지?“라고 물을 필요도 없습니다. 에이전트가 스스로 중단점을 설정하고, 변수 값을 추적하고, 호출 스택을 분석합니다. 내부 MCP 서버를 통해 디버거 상태를 실시간으로 수집하면서 문제의 근본 원인을 찾아나가는 모습은 마치 시니어 개발자가 옆에서 페어 디버깅을 해주는 것 같았습니다.

특히 zlib 압축 해제 문제를 추적할 때 이 기능이 빛났습니다. Java와 C#에서 같은 바이트 배열을 넣었는데 결과가 다르게 나오는 상황. 평소였다면 양쪽 코드에 일일이 중단점을 걸고, 바이트 단위로 비교하며 몇 시간을 써야 했을 겁니다. 하지만 에이전트 기반 디버거는 “이 두 결과가 왜 다른지 추적해줘"라는 요청 하나로 스스로 분석을 시작했고, 헤더 처리 방식의 차이를 짚어냈습니다.

이게 바로 Visual Studio 2026의 moat라고 생각합니다. 코드 생성은 누구나 할 수 있지만, 디버거와 AI의 깊은 통합은 수십 년간 디버깅 도구를 만들어온 Microsoft만이 할 수 있는 영역입니다. 이 기능이 없었다면 이 프로젝트는 3주가 아니라 3달이 걸렸을지도 모릅니다.

그래도 사람이 필요한 순간들

하지만 모든 걸 AI에게 맡길 수는 없었습니다. “Apache POI 대신 뭘 써야 하지?“라는 질문에 AI는 여러 옵션을 제시해줬지만, 실제로 어떤 라이브러리가 프로젝트에 맞는지 판단하고 결정하는 건 제 몫이었습니다.

특히 디버깅은 아직 사람의 영역이라고 느꼈습니다. zlib 압축 해제 결과가 달라지는 문제를 추적할 때, AI에게 “왜 이게 다르게 나오지?“라고 물어봐도 명확한 답을 얻기 어려웠습니다. 결국 바이트 배열을 직접 덤프하고, Java와 C#의 동작을 비교하며 원인을 찾아야 했습니다.

페어 프로그래밍의 새로운 정의

결론적으로, AI는 최고의 주니어 개발자 같았습니다. 시키는 건 정말 잘합니다. 빠르고, 지치지 않고, 불평도 안 합니다. 하지만 “뭘 시킬지” 결정하는 건 여전히 시니어의 몫입니다.

그런데 Visual Studio 2026의 에이전트 기반 디버거는 이 공식을 살짝 바꿔놓았습니다. 코드 생성에서는 AI가 주니어 역할이 맞지만, 디버깅에서는 오히려 AI가 시니어처럼 행동했습니다. 제가 “이거 왜 안 되지?” 하고 막막해할 때, 에이전트가 먼저 가설을 세우고 검증하고, “여기가 문제인 것 같습니다"라고 짚어줬으니까요.

전통적인 페어 프로그래밍은 한 명이 코드를 치고, 다른 한 명이 옆에서 리뷰하는 방식이었습니다. 하지만 AI 시대의 페어 프로그래밍은 다릅니다. 코드 작성은 AI가 치고 사람이 리뷰하고, 디버깅은 AI가 분석하고 사람이 판단합니다. 역할이 상황에 따라 유연하게 바뀌는 거죠.

이 프로젝트를 통해 깨달은 건, AI 시대의 개발자는 코드를 직접 치는 시간보다 “어떤 코드를 쳐야 하는지” 판단하는 시간이 더 중요해졌다는 것입니다. 그리고 좋은 도구는 그 판단을 더 빠르고 정확하게 할 수 있도록 도와줍니다. Visual Studio 2026은 그런 도구였습니다.

마무리하며

3주간의 여정이 끝났습니다. 54개의 커밋, 641개의 파일, 5만 줄 이상의 코드.

지금 libhwpsharp는 NuGet에 배포되어 있고, 누구나 .NET 프로젝트에서 HWP 파일을 다룰 수 있습니다. 물론 아직 갈 길이 멉니다. 실제 필드에서 다양한 HWP 파일을 만나면 분명 예상치 못한 엣지 케이스들이 나올 것입니다.

그래도 첫 발을 뗐다는 것, 그리고 AI와 함께라면 혼자서는 엄두도 못 냈을 규모의 프로젝트도 도전해볼 수 있다는 것을 확인한 것만으로도 의미 있는 경험이었습니다.

혹시 HWP 파일을 .NET에서 다뤄야 하는 분이 계시다면, 한번 써보시고 피드백 부탁드립니다!

앞으로의 계획

libhwpsharp는 시작일 뿐입니다. neolord0 님께서는 hwplib 외에도 여러 유용한 프로젝트를 만들어 두셨는데, 이들도 .NET으로 가져올 계획입니다.

hwpxlib 포팅

첫 번째 목표는 hwpxlib입니다. HWPX는 한글과컴퓨터가 새롭게 도입한 개방형 문서 포맷으로, 기존 HWP의 바이너리 형식 대신 XML 기반의 압축 파일 구조를 사용합니다. 정부 기관을 중심으로 HWPX 채택이 늘어나고 있어서, .NET에서도 이 포맷을 다룰 수 있어야 합니다.

다행히 HWPX는 XML 기반이라 HWP보다 포팅이 수월할 것으로 예상됩니다. OLE2 복합 문서의 늪에서 허우적거렸던 경험을 생각하면, ZIP + XML 조합은 거의 휴양지 수준이죠.

HWP → HWPX 변환 도구

두 번째 목표는 neolord0 님의 HWP to HWPX 변환 도구 포팅입니다. 레거시 HWP 파일을 새로운 HWPX 포맷으로 마이그레이션해야 하는 수요가 분명 있을 것입니다. 이 도구까지 포팅되면, .NET 생태계에서 한글 문서 처리의 전체 파이프라인이 완성됩니다.

이 두 프로젝트도 완성되면 블로그에서 공유하겠습니다. 기대해 주세요!

기술적 세부사항

마지막으로, 개발 과정에서 마주친 주요 기술적 도전들을 정리해 둡니다.

Apache POI → OpenMcdf 전환

구분Java (POI)C# (OpenMcdf)
디렉터리 접근DirectoryEntryCFStorage
스트림 읽기DocumentInputStreamCFStream
리소스 관리close()Dispose() / using

언어별 차이 극복

JavaC#처리 방법
byte (signed)byte (unsigned)sbyte 사용 또는 변환
getXxx() / setXxx()Property자동 변환
Iterator<T>foreachLINQ 활용
throws 선언없음try-catch 패턴

감사의 말

이 프로젝트는 neolord0 (박성균)님의 hwplib 없이는 존재할 수 없었습니다.

HWP 파일 포맷은 공식 문서가 부족하고, 버전별로 미묘한 차이가 있어서 리버스 엔지니어링이 쉽지 않은 영역입니다. 그럼에도 불구하고 박성균 님께서는 수년간 hwplib을 꾸준히 개발하고 유지보수해 오셨습니다. 꼼꼼하게 작성된 유닛 테스트, 명확한 코드 구조, 그리고 Apache License 2.0으로 공개해주신 덕분에 이 포팅 프로젝트가 가능했습니다.

특히 47개의 테스트 케이스는 포팅 과정에서 등대와 같은 역할을 해주었습니다. “이것만 통과하면 된다"는 명확한 목표가 있었기에 3주라는 짧은 기간 안에 완성할 수 있었습니다.

오픈소스 생태계는 이렇게 서로의 어깨 위에 올라서며 성장합니다. 누군가의 묵묵한 헌신이 또 다른 누군가에게 새로운 가능성을 열어줍니다. 박성균 님께서 Java 생태계에 심어주신 씨앗이 이제 .NET 생태계에서도 싹을 틔우게 되었습니다.

진심으로 감사드립니다. 🙏

링크