C# Excel 조작에서 EXCEL.EXE가 남는 문제 ── COM 참조 해제 패턴과 교체 판단

· 업데이트: · · Excel, C#, COM, .NET, .NET Framework, Office, 레거시 유지보수, 기술 상담

수정 이력(5건, 최종 수정 2026년 09월 03일)

이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.

Codex 리뷰에 따라 상담·문의 링크에 /ko/ 로케일 접두를 붙였습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
permalink·저자 표기·지식 맵 래퍼·깨진 내부 링크 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
기사 맨 앞에 「이 기사의 지식 맵」 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 정리한 것입니다. 본문의 주장은 바꾸지 않았습니다.
외부 리뷰(1283건)에 대응하여 본문을 갱신했습니다. 개별 변경 내용은 아래 이력을 참조하십시오.
.NET 변수에서 RCW, COM 참조 카운트, EXCEL.EXE까지의 관계도를 추가하고, `Quit()`와 `ReleaseComObject`가 각각 어디에 작용하는지를 보였습니다. 긴 해제 코드 앞에 `try`/`finally` 골격을 두고, 해결되었는지 확인하는 절차(`tasklist`로 전후 비교, 10회 연속 실행, 예외 경로)를 신설했습니다. 「2-dot 규칙」의 근거를 1차 정보에서 따라갈 수 있게 했습니다.
최초 공개
이 글을 인용하기(DOI(등록된 아카이브): 10.5281/zenodo.21635352)

아래 DOI는 이전에 등록된 아카이브를 가리키며 현재 본문과 다를 수 있습니다. 현재 본문을 참조할 때는 이 페이지의 URL을 사용하세요.

Go Komura (2026). 「C# Excel 조작에서 EXCEL.EXE가 남는 문제 ── COM 참조 해제 패턴과 교체 판단」. 합동회사 코무라소프트. https://comcomponent.com/ko/blog/excel-com-interop-process-remains/

DOI(등록된 아카이브)
10.5281/zenodo.21635352
DOI(마지막 등록 버전)
10.5281/zenodo.21635353

「장표를 Excel로 내보내는 기능을 만들었더니 작업 관리자에 EXCEL.EXE가 줄줄이 늘어 있었다」「앱을 종료했는데도 다음에 파일을 열려고 하면 『다른 프로세스에서 사용 중입니다』라고 나온다」「야간 배치 서버에 Excel 프로세스가 수백 개 쌓여 메모리를 다 잡아먹고 멈췄다」. C#에서 Microsoft.Office.Interop.Excel로 Excel을 조작하는 코드를 작성한 개발자는 거의 모두 이 현상을 한 번은 겪습니다. 당사에도 「Quit()를 호출했는데 Excel이 종료되지 않는다」는 상담이 정기적으로 들어옵니다.

까다로운 점은 이 문제가 「가끔만 일어나는 것처럼」 보인다는 것입니다. 개발 머신에서는 사라지는데 운영에서는 남고, 디버그 실행에서는 남는데 릴리스에서는 사라집니다. 재현 조건이 흔들리기 때문에 대증 요법의 Process.Kill이 운영 코드에 섞여 들어가기 쉽습니다. 그러나 원인은 운이 아니라 COM 참조 카운트와 .NET의 RCW(Runtime Callable Wrapper) 구조로 완전히 설명할 수 있습니다. 이 기사에서는 남는 메커니즘, 대표적인 함정인 「2-dot 규칙」, 해제 패턴 두 갈래의 정리와 당사 권장, 최후 수단인 프로세스 Kill의 올바른 방법, Open XML 계열 라이브러리로의 교체 판단까지를 실무 관점에서 정리합니다.

1. 먼저 결론

한 줄로 말하면, Quit()는 해제가 아니므로 RCW가 붙잡고 있는 COM 참조가 모두 반환될 때까지 EXCEL.EXE는 종료하지 않습니다. 아래가 그 내역과 대책입니다.

  • EXCEL.EXE가 남는 것은 버그가 아니라 .NET 쪽이 아직 COM 참조를 붙잡고 있기 때문입니다. Quit()는 「모든 참조가 해제되면 종료한다」는 요청에 지나지 않으며, 참조가 남아 있으면 Excel은 그대로 기다립니다.
  • .NET은 COM 객체를 RCW(Runtime Callable Wrapper)라는 래퍼를 통해 다루며, RCW가 GC에 회수될 때까지(또는 명시적으로 해제될 때까지) COM 객체에 대한 참조를 계속 유지합니다. 1
  • book.Worksheets[1].Range["A1"]처럼 점을 두 개 이상 이으면 중간 객체의 RCW가 어떤 변수에도 들어가지 않은 채로 생성되어 해제 누수가 됩니다. 통칭 「2-dot 규칙」이며, 이 문제의 주원인입니다.
  • 해제 패턴은 두 갈래가 있습니다. (a) 모든 COM 객체에 Marshal.ReleaseComObject를 규율 있게 적용하는 쪽과 (b) 참조를 변수에 가두고 GC(GC.Collect + WaitForPendingFinalizers)에 회수시키는 쪽입니다. 공식 문서는 ReleaseComObject를 「반드시 필요한 경우에만 사용한다」고 명시하고 있습니다. 2
  • 당사 권장은 처리를 하나의 메서드에 격리한 GC 패턴을 기본으로 하고, 해제 순서 제어가 필요한 경우에만 using으로 해제를 규율화하는 작은 래퍼를 도입하는 것입니다(4장).
  • 그래도 남는 경우의 보험으로는 Application.Hwnd에서 윈도우 핸들을 미리 받아 두고 GetWindowThreadProcessId로 PID를 특정해 Kill합니다. 기동 전후 프로세스 목록의 차이로 맞추는 방법은 사용자가 열어 둔 Excel을 함께 죽이는 사고로 이어지므로 쓰지 않습니다. 34
  • 애초에 서버 사이드·무인 환경에서의 Office 자동화는 Microsoft가 지원하지 않습니다. 5 무인 실행의 장표 생성이라면 Excel을 기동하지 않는 Open XML SDK / ClosedXML로의 교체를 먼저 검토하십시오(6~7장). 67

그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 24건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle

2. 왜 EXCEL.EXE가 남는가 ── 참조 카운트와 RCW

먼저 COM 쪽 원칙입니다. COM 객체의 수명은 참조 카운트로 관리되며, Excel의 자동화 서버(EXCEL.EXE)는 외부 클라이언트에 넘긴 참조가 모두 반환될 때까지 종료하지 않습니다. VB6 시절에도 Release를 호출하지 않으면 마찬가지로 프로세스가 남았습니다(COM 복습은 「COM / ActiveX / OCX란 무엇인가」로).

다음으로 .NET 쪽입니다. C# 코드는 COM 객체를 직접 건드리지 않고, CLR이 생성하는 RCW라는 프록시를 통해 조작합니다. RCW는 COM 객체 하나당 프로세스 안에 하나 만들어지고, COM 인터페이스 포인터를 캐시하며, 자신이 GC에 회수될 때 COM 객체에 대한 참조를 해제합니다. 1 즉 수명 관리가 「참조 카운트를 직접 세는」 방식에서 「GC에 맡기는」 방식으로 바뀐 것입니다.

이 둘을 합치면 EXCEL.EXE가 남는 이유가 그대로 나옵니다. 참조의 연결을 그림으로 그리면 다음과 같습니다.

EXCEL.EXE 안.NET 프로세스 안COM 참조를 계속 붙잡음여기가 효과가 있음여기에는 효과가 있으나 해제는 하지 않음COM 참조 카운트밖으로 넘긴 참조가 모두 돌아올 때까지 줄지 않음EXCEL.EXE는 종료하지 않음변수excel / book / sheet익명 중간 객체book.Worksheets 등의 반환값변수가 없어 손으로 해제할 수 없음(3장)RCWCOM 객체 하나당 하나GC로 회수될 때 COM 참조를 반환Marshal.ReleaseComObject또는 GC에 의한 회수(4장)excel.Quit()「종료해도 된다」고 전할 뿐참조는 하나도 줄지 않음

왼쪽 끝의 「변수」와 「익명 중간 객체」 어느 쪽에서 오든, RCW가 COM 참조를 붙잡고 있는 한 오른쪽 끝은 종료하지 않습니다. 효과가 있는 것은 그림 아래쪽 두 선 가운데 REL에서 R로 뻗은 쪽뿐입니다. 단계를 따라가면 다음과 같습니다.

단계 일어나는 일
new Excel.Application() EXCEL.EXE가 기동하고 Application의 RCW가 만들어집니다
셀 조작·저장 등 Workbook, Worksheet, Range … 다룬 객체마다 RCW가 늘어갑니다
excel.Quit() Excel에 「종료해도 된다」고 전할 뿐입니다. RCW가 붙잡고 있는 참조는 하나도 줄지 않습니다
메서드를 빠져나옴 RCW에 대한 .NET 참조는 없어지지만, RCW 자체는 아직 힙에 살아 있습니다
(언젠가의) GC RCW가 회수되고, 그때 처음으로 COM 참조가 반환됩니다 → EXCEL.EXE가 종료합니다

포인트는 두 가지입니다. 첫째, Quit()는 해제가 아니다는 점입니다. 참조가 남아 있는 한 Excel은 기다립니다. 앱 프로세스가 완전히 종료하면 참조가 끊겨 Excel도 종료하는 것이 보통이지만, 상주 앱이나 웹 앱처럼 부모 프로세스가 계속 살아 있는 형태에서는 그 「언젠가」가 오지 않습니다. 둘째, 해제 시점이 GC에 의존해 정해지지 않는다는 점입니다. 메모리에 여유가 있으면 GC는 몇십 분이고 돌지 않고, 그 동안 EXCEL.EXE는 좀비로 남습니다. 「가끔 남는다」「운영에서만 남는다」는 재현성 없는 현상은 GC 타이밍의 흔들림이 그대로 보이는 것뿐입니다.

또한 Visible = false로 기동한 Excel은 윈도우가 없으므로, 남아 있어도 사용자에게는 보이지 않습니다. 증상이 「두 번째 저장에서 『파일이 사용 중』 오류」「PC가 느리다」로 나타나고, 작업 관리자에서 비로소 EXCEL.EXE 행렬을 알아차리는 것이 전형입니다. 조사의 첫 단계는 tasklist | findstr EXCEL로 개수를 세는 것입니다.

3. 대표적인 함정 「2-dot 규칙」 ── 보이지 않는 중간 객체

「변수는 전부 해제하고 있는데도 남는다」는 상담 코드에는 거의 반드시 다음 같은 줄이 있습니다.

// 겉보기에는 깔끔하지만, 해제할 수 없는 RCW를 만들고 있다
excel.Workbooks.Open(path);
book.Worksheets[1].Range["A1"].Value2 = "hello";

excel.Workbooks는 Workbooks 컬렉션의 RCW를 생성해 반환합니다. 이 반환값을 변수에 받지 않고 .Open(...)을 호출하면, Workbooks의 RCW는 「아무도 참조하지 않지만 살아 있는」 익명 객체로 힙에 남습니다. 변수가 없으므로 Marshal.ReleaseComObject를 호출할 수단도 없습니다. 두 번째 줄은 더 심각해서, Worksheets(컬렉션), Worksheets[1](시트), Range["A1"](Range)처럼 한 줄에서 익명 RCW 세 개를 만듭니다.

이를 피하는 경험 규칙이 Office 자동화 커뮤니티에서 오래전부터 말하는 「2-dot 규칙」입니다. COM 객체에 점을 두 개 이상 잇지 않는다. 모든 중간 객체를 한 번 변수에 받는다고 바꿔 말할 수 있습니다.

// 모든 중간 객체에 이름을 붙인다
Excel.Workbooks books = excel.Workbooks;
Excel.Workbook book = books.Open(path);
Excel.Sheets sheets = book.Worksheets;
Excel.Worksheet sheet = (Excel.Worksheet)sheets[1];
Excel.Range cell = sheet.Range["A1"];
cell.Value2 = "hello";

장황해 보이지만, 이것은 「해제해야 할 것을 모두 열거할 수 있는 상태로 만들기」 위한 작성법입니다. 놓치기 쉬운 파생 패턴도 적어 둡니다.

  • foreach: foreach (Excel.Worksheet s in book.Worksheets)는 컬렉션과 열거자, 나아가 각 요소의 RCW를 생성합니다. ReleaseComObject 쪽에서는 인덱스 for 루프로 한 건씩 변수에 받는 것이 정석입니다.
  • 조건식 안에서 값을 버리고 읽기: if (excel.Workbooks.Count > 0) 같은 식 안에서도 RCW는 생깁니다.
  • 복합식 인수: sheets.Add(After: sheets[sheets.Count]) 같은 식은 한 줄에서 여러 익명 RCW를 만듭니다.
  • 이벤트 구독: Application이나 Workbook 이벤트에 핸들러를 붙이면 그 연결이 참조를 유지합니다. 종료 전에 반드시 구독을 해제합니다.

마지막으로 출처 이야기입니다. 「2-dot 규칙」이라는 이름 자체는 커뮤니티의 것이고, Microsoft의 공식 용어는 아닙니다. 다만 규칙의 내용에는 1차 정보의 뒷받침이 있습니다. Office 앱이 자동화 후에 종료하지 않는 문제를 다룬 Microsoft 지원 문서는, 종료시키기 위한 조건의 첫머리에 「각 객체를 새 변수로 선언한다」를 들고, oExcel.Workbooks.Add()처럼 이어 쓰지 말고 oBooks = oExcel.Workbooks를 한 번 끼우는 작성법을 보입니다. 8 이것과 「RCW는 COM 객체 하나당 하나 만들어지고, GC에 회수될 때 참조를 해제한다」1를 나란히 놓으면, 왜 중간 객체가 새는지 1차 정보만으로 따라갈 수 있습니다. 같은 문서는 해제해도 종료하지 않을 때의 처치로 GC.Collect와 GC.WaitForPendingFinalizers를 호출하는 것도 언급합니다. 8 다음 장의 두 가지 방식은 어느 쪽이든 이 문서의 연장선에 있다고 보면 됩니다.

4. 해제 패턴 정리 ── ReleaseComObject 쪽과 GC 쪽

EXCEL.EXE를 확실히 종료시키는 작성법은 두 가지 방식이 있습니다. 어느 쪽이든 올바르게 쓰면 동작합니다. 문제는 「올바르게 계속 쓸 수 있는가」이며, 여기에 실무상의 차이가 납니다.

4.1 (a) Marshal.ReleaseComObject를 규율 있게 적용하는 쪽

Marshal.ReleaseComObject는 RCW의 내부 참조 카운트를 감소시키고, 0이 된 시점에 RCW가 붙잡은 COM 참조를 즉시 해제합니다. 2 GC를 기다리지 않고 결정적인 타이밍에 해제할 수 있는 것이 이점입니다.

코드가 길어지므로 먼저 골격만 봅니다. 할 일은 「사용한 것을 전부 변수에 받고 생성의 역순으로 해제한다」이지만, Close나 Quit 자체도 COM 호출이라 실패할 수 있습니다. 그래서 중간에 예외가 나와도 이후 해제에 반드시 도달하도록 try/finally를 중첩하는 것이 요점입니다.

try
    Application → Workbooks → Workbook → Sheets → Worksheet → Range  ← 생성 순
finally
    Range·Worksheet·Sheets를 해제                                   ← 생성의 역순
    try   book.Close()
    finally
        Workbook·Workbooks를 해제
        try   excel.Quit()
        finally
            Application을 해제

이것을 그대로 C#으로 풀어 쓰면 다음과 같습니다. 길어 보이는 것은 이 중첩과 null 검사를 생략하지 않고 전부 적었기 때문입니다.

using Excel = Microsoft.Office.Interop.Excel;
using System.Runtime.InteropServices;

Excel.Application excel = null;
Excel.Workbooks books = null;
Excel.Workbook book = null;
Excel.Sheets sheets = null;
Excel.Worksheet sheet = null;
Excel.Range cell = null;
try
{
    // 객체 이니셜라이저(new ... { DisplayAlerts = false })는 쓰지 않는다.
    // setter의 COM 호출이 실패하면 excel이 할당되지 않은 채로 finally에 들어가,
    // 이미 기동한 EXCEL.EXE에 대해 Quit도 해제도 할 수 없게 되기 때문
    excel = new Excel.Application();
    excel.DisplayAlerts = false;
    books = excel.Workbooks;
    book = books.Open(templatePath);
    sheets = book.Worksheets;
    sheet = (Excel.Worksheet)sheets[1];
    cell = sheet.Range["A1"];
    cell.Value2 = "hello";
    book.SaveAs(outputPath);
}
finally
{
    // 생성의 역순으로 해제한다. Close나 Quit 자체도 COM 호출이라 실패할 수 있으므로,
    // 중간에 예외가 나와도 Quit와 해제에 반드시 도달하도록 중첩 try/finally로 둔다
    if (cell   != null) Marshal.ReleaseComObject(cell);
    if (sheet  != null) Marshal.ReleaseComObject(sheet);
    if (sheets != null) Marshal.ReleaseComObject(sheets);
    try
    {
        if (book != null) book.Close(SaveChanges: false);
    }
    finally
    {
        if (book  != null) Marshal.ReleaseComObject(book);
        if (books != null) Marshal.ReleaseComObject(books);
        try
        {
            if (excel != null) excel.Quit();
        }
        finally
        {
            if (excel != null) Marshal.ReleaseComObject(excel);
        }
    }
}

이 방식의 약점은 보다시피 규율 비용이 높다는 것입니다. 다룬 COM 객체를 하나도 빠짐없이 변수에 받고, 예외 경로까지 포함해 역순으로 해제합니다. 게다가 Close나 Quit 자체가 실패하는(COM 오류, 이미 끊긴 Workbook, 응답하지 않는 Excel) 경우를 생각하면, 위처럼 중첩 try/finally로 「중간에 실패해도 이후 해제에 도달한다」는 것까지 보장해야 합니다. 이를 전 멤버가 모든 수정에서 계속 지키는 것은 경험상 꽤 어려운 요구입니다. 한 곳이라도 2-dot 복합식이 끼어들면 누수가 다시 살아납니다.

더 중요한 것은 공식 문서 자신이 남용에 경고를 내고 있다는 점입니다. Marshal.ReleaseComObject 레퍼런스에는, 리소스를 제때 해제해야 하는 경우나 해제 순서에 의미가 있는 경우를 위한 수단이며 「반드시 필요한 경우에만 사용할 것(use the ReleaseComObject only if it is absolutely required)」이라고 명시되어 있습니다. 2 RCW는 프로세스 안에서 COM 객체 하나당 하나를 공유하는 구조이므로, 코드의 한곳에서 해제한 RCW를 다른 곳이 아직 쓰고 있으면 InvalidComObjectException이나, 최악의 경우 액세스 위반·프로세스 메모리 파괴로 이어집니다. 2 Excel 조작을 앱 안의 여러 모듈이 공유하는 구조에서는 이 사고가 실제로 일어납니다.

또한 같은 인터페이스 포인터가 CLR에 여러 번 넘어가면 RCW의 참조 카운트가 1을 넘을 수 있고, 그 경우 한 번의 호출로는 해제되지 않습니다. 카운트를 강제로 0으로 만드는 Marshal.FinalReleaseComObject도 있지만2, 이 API가 필요해진 시점에서 수명 관리를 파악하지 못했다는 신호이므로, 당사는 설계 재검토를 권합니다.

프로세스 잔류와는 다른 축의 보안상 주의도 하나 있습니다. COM 자동화로 Workbooks.Open한 Workbook은 매크로 경고 없이 VBA가 실행될 수 있습니다. 이 기사의 샘플은 자사 앱이 관리하는 신뢰된 템플릿을 연다는 전제이지만, 공유 폴더의 파일이나 이용자가 업로드한 파일처럼 외부에서 온 Workbook을 열 가능성이 있다면 Open 전에 excel.AutomationSecurity = MsoAutomationSecurity.msoAutomationSecurityForceDisable(Microsoft.Office.Core 네임스페이스)을 설정해 매크로를 강제 무효화하십시오. 9 템플릿 교체가 곧 임의 코드 실행이 되는 종류의 공격을 막을 수 있습니다. 이 주의는 뒤에 나오는 GC 패턴 샘플에도 그대로 해당합니다.

4.2 (b) 참조를 가두고 GC에 회수시키는 쪽

또 하나의 방식은 RCW 해제를 본래 구조대로 GC에 맡기고, 그 GC를 확정적인 타이밍에 돌리는 방식입니다. RCW는 GC에 회수될 때 COM 참조를 해제하므로1, 「Excel을 다루는 모든 참조가 스코프를 빠져나간 뒤에 풀 GC + finalizer 완료 대기를 수행」하면, ReleaseComObject를 한 번도 쓰지 않고 EXCEL.EXE를 종료시킬 수 있습니다.

using System.Runtime.CompilerServices;
using Excel = Microsoft.Office.Interop.Excel;

public void ExportReport(string templatePath, string outputPath)
{
    try
    {
        // Excel을 다루는 처리는 별도 메서드에 완전히 격리한다
        ExportReportCore(templatePath, outputPath);
    }
    finally
    {
        // 예외 경로야말로 EXCEL.EXE가 남기 쉬우므로 반드시 finally에서 실행한다.
        // 메서드를 빠져나온 뒤라면 RCW에 대한 참조는 어디에도 남아 있지 않다
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();   // finalizer가 떼어 낸 RCW를 회수하는 두 번째 바퀴
    }
}

[MethodImpl(MethodImplOptions.NoInlining)]
private void ExportReportCore(string templatePath, string outputPath)
{
    var excel = new Excel.Application();
    try
    {
        excel.DisplayAlerts = false;
        Excel.Workbooks books = excel.Workbooks;
        Excel.Workbook book = books.Open(templatePath);
        Excel.Sheets sheets = book.Worksheets;
        Excel.Worksheet sheet = (Excel.Worksheet)sheets[1];
        Excel.Range cell = sheet.Range["A1"];
        cell.Value2 = "hello";
        book.SaveAs(outputPath);
        book.Close(SaveChanges: false);
    }
    finally
    {
        excel.Quit();
    }
}

성립 조건은 세 가지입니다.

  1. Excel을 다루는 코드를 하나의 메서드에 격리한다는 점입니다. JIT가 스택 위의 참조를 살려 두는 동안은 GC가 회수할 수 없으므로, GC는 반드시 「Excel을 다룬 메서드 밖」에서 호출합니다. NoInlining은 인라인으로 격리가 무효화되는 것을 막기 위함입니다.
  2. RCW를 필드나 반환값으로 내보내지 않는다는 점입니다. 하나라도 메서드 밖으로 새면, 그 참조가 살아 있는 한 Excel은 남습니다.
  3. GC.Collect → GC.WaitForPendingFinalizers → GC.Collect의 3점 세트로 둔다는 점입니다. RCW 뒷정리는 finalizer를 통해 이루어지므로, 첫 Collect에서 검출하고, finalizer 완료를 기다린 뒤, 두 번째 Collect로 잔해를 회수하는 두 바퀴가 정석입니다.

주의할 점은, 디버거를 붙인 실행에서는 변수 수명이 메서드 끝까지 연장되므로 이 방식으로도 RCW가 회수되지 않을 수 있다는 것입니다. 「디버그에서는 남는데 릴리스에서는 사라진다」는 현상의 정체가 이것이며, 동작 확인은 반드시 릴리스 빌드·디버거 없이 하십시오.

이 방식의 이점은 2-dot 규칙을 깨더라도 누수되지 않는다는 것입니다. 익명 중간 RCW를 포함해, 참조가 끊긴 것은 GC가 한꺼번에 회수합니다. 리뷰에서 볼 지점이 「Excel을 다루는 처리가 이 메서드 안에 있는가」 한 점으로 모이므로, 규율 비용이 극적으로 내려갑니다. 단점은 GC.Collect의 명시 호출이라는 코드 냄새(앱 전체 풀 GC로 인한 일시 정지)와, 이유를 모르는 멤버가 선의의 리팩터링으로 깨뜨릴 위험입니다. 3점 세트에는 반드시 이유를 주석으로 남겨 두십시오.

4.3 당사 권장 ── 격리+GC를 기본으로, 필요하면 래퍼로 규율화

양쪽을 실무 관점에서 비교합니다.

  (a) ReleaseComObject 쪽 (b) GC 쪽
해제 타이밍 결정적(호출한 순간) 준결정적(GC 3점 세트의 순간)
규율 비용 높음. 전 객체의 변수화+역순 해제를 전원이 지켜야 함 낮음. 메서드 격리만 지키면 됨
지나친 경우의 증상 InvalidComObjectException, 액세스 위반2 GC 강제로 인한 일시 정지
공식 가이드와의 정합 「반드시 필요한 경우만」2 RCW 본래의 수명 관리(GC에 맡김)에 맞음1
맞는 장면 해제 순서에 의미가 있는 경우. 장수명 프로세스에서 Excel을 잘게 여러 번 쓰는 경우 「열고·쓰고·닫기」를 한곳에서 끝낼 수 있는 대부분의 장표 처리

당사 권장은 기본은 (b)의 격리+GC 패턴입니다. 장표 출력 같은 처리는 하나의 메서드에 가두는 것이 자연스럽고, 그 형태만 되면 누수가 구조적으로 일어나지 않습니다. ReleaseComObject의 남용 위험도 피하고, 공식 문서의 입장과도 맞습니다.

해제 순서와 즉시성이 정말로 필요한 경우에만 (a)를 쓰고, 그 경우에는 날것의 ReleaseComObject를 쓰게 하지 않고 using으로 규율화한 작은 래퍼를 도입합니다.

using System.Runtime.InteropServices;

/// <summary>COM 객체를 using 스코프로 해제하는 래퍼</summary>
public readonly struct ComScope<T> : IDisposable where T : class
{
    public T Value { get; }
    public ComScope(T value) => Value = value;

    public void Dispose()
    {
        if (Value is not null && Marshal.IsComObject(Value))
            Marshal.ReleaseComObject(Value);
    }
}
using var books = new ComScope<Excel.Workbooks>(excel.Workbooks);
using var book  = new ComScope<Excel.Workbook>(books.Value.Open(templatePath));
using var sheets = new ComScope<Excel.Sheets>(book.Value.Worksheets);
using var sheet = new ComScope<Excel.Worksheet>((Excel.Worksheet)sheets.Value[1]);
using var cell  = new ComScope<Excel.Range>(sheet.Value.Range["A1"]);
cell.Value.Value2 = "hello";
book.Value.SaveAs(outputPath);
book.Value.Close(SaveChanges: false);

using의 선언 순서와 역순으로 Dispose가 돌므로, 「생성의 역순으로 해제」가 언어 구조로 보장됩니다. .Value만큼 서술은 늘어나지만, 「COM 객체는 반드시 ComScope로 받는다」는 한 줄 규약으로 규율을 압축할 수 있습니다. 반대로 말하면 books.Value.Open(...).Worksheets 같은 복합식을 쓰면 누수는 다시 살아나므로, 2-dot 규칙 교육은 어차피 필요합니다.

어느 패턴이든 공통 주의가 두 가지입니다. 첫째, Quit() 전에 저장 확인 대화 상자를 띄우지 않는다는 점입니다. DisplayAlerts = false와 Close(SaveChanges: false)를 명시합니다. 숨긴 Excel이 대화 상자 대기로 행하면 Quit 자체가 완료되지 않습니다. 둘째, Excel의 COM은 STA 전제이므로 여러 스레드에서 같은 Application을 돌려 쓰지 않는다는 점입니다. 스레드와 COM 아파트먼트의 관계는 「COM STA/MTA의 기초 지식」에서 정리했습니다.

4.4 고쳐졌는지 확인하는 절차

해제 패턴을 넣었으면 「정말로 사라지게 되었는가」의 확인 절차까지 정해 둡니다. 이 문제는 재현 조건이 흔들리므로, 절차를 고정하지 않으면 「우연히 사라진 한 번」을 성공으로 오인합니다.

  1. 릴리스 빌드로, 디버거를 붙이지 않고 실행한다. 4.2절에서 말했듯, 디버거를 붙이면 변수 수명이 메서드 끝까지 늘어나 올바르게 쓴 GC 패턴에서도 RCW가 회수되지 않습니다. Visual Studio라면 「디버그하지 않고 시작」입니다.
  2. 시작 전 EXCEL.EXE 개수를 센다. tasklist /FI "IMAGENAME eq EXCEL.EXE"를 실행해, 이미 돌아가고 있는 Excel(이용자가 손으로 연 것 포함)의 개수를 적어둡니다. PowerShell이라면 @(Get-Process EXCEL -ErrorAction SilentlyContinue).Count로 개수만 얻을 수 있습니다.
  3. 같은 처리를 10회 이상 연속 실행한다. 한 번만으로는 GC 타이밍에 따라 우연히 사라질 수 있습니다.
  4. 처리가 끝나고 몇 초 기다린 뒤 다시 센다. 절차 2와 같은 개수로 돌아왔으면 성공입니다. 늘었다면 늘어난 개수가 곧 샌 횟수입니다.
  5. 예외 경로에서도 같은 절차를 돌린다. 템플릿 경로를 존재하지 않는 것으로 만들거나 저장 위치를 읽기 전용으로 두는 등 중간에 일부러 실패시키고, 그래도 개수가 돌아오는지 확인합니다. EXCEL.EXE가 남는 사고의 대부분은 정상 경로가 아니라 예외 경로에서 일어나므로, 여기를 건너뛰면 확인한 것이 아닙니다.
  6. 상주 앱이라면 앱을 종료시키지 않고 센다. 프로세스가 종료하면 참조가 끊겨 Excel도 내려가므로, 앱을 닫은 뒤에 세면 아무것도 검증하지 않은 것입니다(2장).

절차 2와 4의 개수를 로그에 남겨 두면, 나중에 해제 코드가 퇴행했을 때 알아챌 수 있습니다. 5장의 보험 Kill을 넣어 둔 경우에는 Kill이 발동하지 않았다는 것(발동 시 경고 로그가 나오지 않았다는 것)까지 확인해야 비로소 「해제가 올바르게 먹혔다」고 말할 수 있습니다.

5. 최후 수단 ── Hwnd에서 PID를 특정해 확실히 정리한다

해제 패턴을 올바르게 구현해도 「추가 기능 사정으로 Excel이 종료하지 않는다」「예외의 이상 경로에서 어떻게든 프로세스 하나가 남는 경우가 있다」 같은 케이스는 남습니다. 무인 실행 배치에서는 남은 프로세스 하나가 다음날 작업의 파일 잠금을 일으키므로, 마지막 보험으로서의 프로세스 Kill을 넣어 둘 가치가 있습니다. 문제는 「어느 EXCEL.EXE를 죽일 것인가」의 특정 방법입니다.

자주 보는 실수는 기동 전후 프로세스 목록의 차이로 특정하는 방법입니다. Process.GetProcessesByName("EXCEL")을 기동 전후로 비교해 늘어난 분을 자기 인스턴스로 보는 방식은 동시성에 취약합니다. 차이를 내는 사이에 사용자가 손으로 Excel을 열면 오검출하고, 같은 방식의 작업이 병렬 실행되면 서로를 헷갈립니다. 사용자가 편집 중인 미저장 Excel을 Kill해 데이터를 지우는 것이 이 방법의 최악의 사고이며, 실제로 상담으로 들어온 적도 있습니다.

올바른 특정 방법은 자신이 기동한 Application 객체의 Hwnd 속성으로 최상위 윈도우 핸들을 얻고, Win32 API GetWindowThreadProcessId로 그 윈도우를 만든 프로세스 ID를 얻는 것입니다. 34 핸들은 자기 인스턴스에 고유하므로 다른 Excel과 헷갈릴 여지가 없습니다.

using System.Diagnostics;
using System.Runtime.InteropServices;
using Excel = Microsoft.Office.Interop.Excel;

internal static class NativeMethods
{
    [DllImport("user32.dll")]
    internal static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
}

public void ExportReport(string templatePath, string outputPath)
{
    // out 인수라면 Excel 조작 도중에 예외가 나와도 핸들이 호출 쪽에 남아,
    // 예외 경로(보험이 정말로 필요한 경로)에서도 GC와 Kill에 반드시 도달한다
    Process excelProcess = null;
    try
    {
        ExportReportCore(templatePath, outputPath, out excelProcess);
    }
    finally
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        if (excelProcess != null)
        {
            KillIfStillAlive(excelProcess);   // 정상 종료했으면 아무것도 하지 않는 보험
            excelProcess.Dispose();
        }
    }
}

[MethodImpl(MethodImplOptions.NoInlining)]
private void ExportReportCore(string templatePath, string outputPath, out Process excelProcess)
{
    excelProcess = null;
    var excel = new Excel.Application();
    // 기동에 성공하면 Hwnd 취득이나 핸들 오픈이 실패해도
    // Quit()에 반드시 도달하도록, 직후부터 try/finally로 감싼다
    try
    {
        // Quit() 이후에는 윈도우가 사라져 취득할 수 없으므로 기동 직후에 받아 둔다.
        // GetProcessById는 PID 대응만 할 뿐, OS 프로세스 핸들은
        // WaitForExit / Kill 등의 첫 접근 때 지연 오픈된다. 그래서
        // Excel이 살아 있는 동안 SafeHandle에 닿아, 여기서 강제로 핸들을 열어 둔다
        // (핸들이 열려 있는 동안 이 PID는 다른 프로세스에 재사용되지 않는다)
        NativeMethods.GetWindowThreadProcessId((IntPtr)excel.Hwnd, out uint pid);
        excelProcess = Process.GetProcessById((int)pid);
        _ = excelProcess.SafeHandle;

        excel.DisplayAlerts = false;
        // …… Excel 조작 ……
    }
    finally
    {
        excel.Quit();
    }
}

private void KillIfStillAlive(Process excelProcess)
{
    if (!excelProcess.WaitForExit(5000))   // 정상이면 수 초 안에 사라진다
    {
        logger.LogWarning("EXCEL.EXE (PID {Pid}) 가 종료하지 않아 강제 종료합니다", excelProcess.Id);
        excelProcess.Kill();
    }
}

구현상의 주의점입니다.

  • Hwnd는 기동 직후에 받아 둔다는 점입니다. Quit() 이후에는 윈도우가 파괴되어 취득할 수 없습니다. Application.Hwnd는 Visible = false여도 취득할 수 있습니다. 3
  • Kill은 「해제를 올바르게 한 위의 보험」입니다. 처음부터 여기에 기대면 Excel 임시 파일 뒷정리가 돌지 않아, 다음 기동 때 자동 복구 파일이 쌓이는 등을 부릅니다. 순서는 반드시 「해제 → Quit → 대기 → 그래도 살아 있으면 Kill」입니다.
  • PID 재사용에 주의합니다. Windows의 PID는 재사용되므로, PID 숫자만 기억해 두었다가 나중에 해석하는 설계면, 그 사이에 Excel이 종료하고 같은 PID가 다른 프로세스에 할당된 경우 무관한 프로세스를 Kill할 위험이 있습니다. 여기서 Process.GetProcessById를 호출하는 것만으로는 부족하다는 점에 주의하십시오. OS 프로세스 핸들은 WaitForExit / Kill 등의 첫 접근 때 지연 오픈되므로, 위 코드처럼 Excel이 살아 있는 동안 SafeHandle에 닿아 명시적으로 핸들을 열어 두어야 비로소 혼동을 막을 수 있습니다(핸들이 열려 있는 동안 그 PID는 재사용되지 않습니다).
  • 이 보험이 커버하는 것은 메서드가 돌아왔는데 EXCEL.EXE가 남아 있는 케이스입니다. Workbooks.Open이나 SaveAs, Quit처럼 COM 호출 자체가 행한 경우(숨은 모달 대화 상자나 추가 기능의 응답 대기)는 finally에 도달하지 않아 이 Kill도 발동하지 않습니다. 대책은 두 층으로 생각합니다. 먼저 DisplayAlerts = false와 앞에서 말한 AutomationSecurity로 대화 상자 요인을 막습니다. 그 위에 무인 배치에서는 Excel을 다루는 작업 자체를 별도 프로세스로 두고, 바깥에서 시간 제한을 걸어 통째로 Kill할 수 있는 자세(작업 스케줄러의 「중지할 때까지의 시간」 설정이나, 부모 프로세스에서 자식 프로세스 Kill)를 취합니다. 프로세스 안의 타임아웃은 행한 COM 호출을 중단할 수 없으므로, 경계는 프로세스에 두는 것이 확실합니다.
  • Kill이 발동하면 반드시 로그에 남겨 빈도를 감시합니다. 발동이 늘어나면 해제 코드의 퇴행이거나, 7장의 교체 판단 신호입니다.

6. 근본 이야기 ── 서버 사이드 Office 자동화는 미지원

여기까지의 기법으로 클라이언트 앱의 EXCEL.EXE 잔류는 거의 제압할 수 있습니다. 그러나 이 문제가 가장 심각해지는 장면──서버나 무인 환경에서의 Excel 자동화──에 대해서는, 기법 전에 확인할 공식 견해가 있습니다.

Microsoft는 「Considerations for server-side Automation of Office」라는 문서에서, 무인·비대화형 클라이언트 애플리케이션이나 컴포넌트(ASP, ASP.NET, DCOM, NT 서비스 포함)에서의 Office 자동화를 권장하지 않으며, 지원하지도 않는다고 명시합니다. 5 Office는 대화형 사용자가 있다는 전제로 설계되어 있어, 오류 때 확인 대화 상자를 띄우고 응답을 기다리는 설계, 실행 사용자 프로필을 전제로 하는 컴포넌트, STA 기반이라 재진입 불가인 아키텍처 등, 데스크톱에서는 문제가 되지 않는 전제가 서비스나 IIS 워커 프로세스 위에서는 모조리 날을 세웁니다. 10 「Windows 서비스에서 돌렸더니 운영에서 Open이 돌아오지 않는다」「IIS 위에서 한 달에 한 번 대화 상자 대기로 행한다」 같은 상담은 모두 이 미지원 구성이 원인이었습니다. 이 구성에서의 EXCEL.EXE 잔류는 단순한 뒷정리 문제가 아니라, 행한 프로세스가 무한히 쌓여 가는 운영 문제가 됩니다.

Microsoft가 대안으로 드는 것이 Office를 설치·기동하지 않고 파일을 직접 다루는 Open XML 파일 형식 편집과, 클라우드 쪽에서 처리하는 Microsoft Graph API입니다. 10 Open XML SDK는 ECMA-376 / ISO/IEC 29500으로 표준화된 Office 파일 형식(.xlsx 등)을 강력한 형식의 클래스로 읽고 쓸 수 있는 Microsoft 제작 라이브러리이며, ZIP과 XML 위에 구축되어 있어 Excel 본체가 필요 없습니다. 6 프로세스 잔류·라이선스·미지원 구성 문제가 한꺼번에 사라집니다.

다만 Open XML SDK는 「파일 형식을 직접 편집하는」 라이브러리이며, Excel이라는 애플리케이션의 동작은 제공하지 않습니다. 공식 디자인 고려 사항으로, 수식 재계산·데이터 갱신 같은 애플리케이션 동작이나 다른 형식(PDF 등)으로의 변환 기능은 제공하지 않는다고 명시되어 있습니다. 11 또한 API는 파일 형식 구조에 충실하므로, 날것의 SDK로 셀 하나를 쓰는 일에도 SpreadsheetML 구조 이해가 필요합니다. 그 자리를 메우는 것이 ClosedXML이며, Open XML API 위에 「Workbook·시트·셀」이라는 직관적 API를 씌운 MIT 라이선스 OSS입니다. Excel 설치 없이 .xlsx / .xlsm을 다룰 수 있습니다(구형식 .xls는 대상 밖). 7 장표 출력 맥락에서의 역할 분담이나 템플릿 방식 설계는 「Excel 장표 출력 만드는 법」에서 자세히 썼습니다.

또한 Microsoft 365에는 무인 RPA용 라이선스(unattended license)가 있지만, 이것은 무인 실행을 라이선스상 가능하게 할 뿐이며, 동작은 여전히 “AS IS”──설계 밖 사용에 따른 예기치 않은 거동은 앱 쪽에서 흡수하라는 위치입니다. 10 「라이선스를 사면 지원되는 구성이 된다」는 것이 아니라는 점은 오해가 많으니 주의하십시오.

7. 판단표 ── COM을 계속 쓸 것인가, Open XML 계열로 바꿀 것인가

이상을 바탕으로 하면, 「Excel을 COM으로 계속 조작할 것인가, COM에서 벗어날 것인가」는 다음 세 갈래로 정리할 수 있습니다.

  (1) COM Interop을 계속 쓴다(래핑해 규율화) (2) Open XML SDK / ClosedXML로 교체 (3) 설계 자체를 다시 본다(Graph 등)
Excel 본체 필요(실행 환경마다 라이선스도) 불필요 불필요
무인·서버 실행 미지원 구성5 문제 없음(권장 대안)10 문제 없음
프로세스 잔류 위험 있음(이 기사의 기법으로 관리) 없음(프로세스를 기동하지 않음) 없음
매크로(VBA) 실행 가능 불가(유지도 라이브러리 의존 ── 아래 2.) 불가
수식 재계산·인쇄·PDF 변환 가능 불가11 Graph에 일부 있음
사용자가 연 Excel과의 대화 가능 불가 불가
구형식 .xls (BIFF) 읽고 쓸 수 있음 불가(.xlsx / .xlsm만)7 ──
실행 속도·병렬성 느림. 병렬에는 인스턴스 분리가 필요10 빠름. 일반 라이브러리로서 병렬 가능 네트워크 의존

판단의 축은 네 가지 질문이 됩니다.

  1. 사용자 눈앞의 Excel과 대화할 필요가 있는가. 「사용자가 연 Workbook에 쓰고, 조작의 나머지를 넘기는」 기능은 COM Interop으로만 만들 수 있습니다. 이 경우는 (1) 한 가지뿐이며, 4장의 규율화에 투자합니다. 데스크톱 대화형 앱은 미지원 구성에도 해당하지 않습니다.
  2. Excel이라는 앱의 기능(매크로 실행, 재계산, 인쇄, PDF 출력)이 필요한가. 이들은 Open XML 계열로 대체할 수 없습니다. 11 무인 실행과의 조합은 가장 괴로운 패턴이며, 요건을 완화할 수 없는지(매크로 로직을 C#으로 이식하기, 계산된 값을 쓰기 등)를 먼저 검토합니다. 주의할 것은 매크로가 들어 있는 템플릿(.xlsm)의 「유지」입니다. Open XML SDK의 저수준 조작이라면 VBA 프로젝트 파트를 건드리지 않는 한 유지되지만, ClosedXML 같은 고수준 라이브러리는 Workbook을 객체 모델로 읽어 패키지를 재구성해 저장하므로 VBA 프로젝트가 사라질 수 있습니다. 매크로 포함 템플릿을 Open XML 계열로 옮길 때는, 실제 템플릿으로 「열고·쓰고·저장한 뒤에도 매크로가 남아 동작하는지」 검증을 필수로 하고, 보장할 수 없으면 그 장표만 COM에 남기십시오. VBA 자산 취급은 「VBA란 무엇인가」의 판단이 그대로 쓰입니다.
  3. 무인 실행인가. 서비스·작업 스케줄러·웹 앱에서의 실행이라면 기본 답은 (2)입니다. 「값과 스타일을 채워 넣은 .xlsx를 만든다」는 장표 생성이 요건의 대부분이며, 그것은 ClosedXML로 끝납니다.
  4. 입출력 형식은 무엇인가. 거래처에서 오는 .xls를 그대로 처리해야 한다면 Open XML 계열은 쓸 수 없습니다. 입구에서 .xlsx 변환을 끼울 수 있는지를 검토합니다.

당사가 실제 건에서 자주 제안하는 것은 「생성은 ClosedXML, Excel 본체가 꼭 필요한 처리만 COM으로 격리한다」는 분할입니다. 매일 수백 장의 장표 생성은 ClosedXML로 서버 실행하고, 매월 「매크로 포함 Workbook 갱신」만 담당자 데스크톱에서 버튼 실행의 COM 처리로 둡니다. 이렇게 하면 무인 환경에서 COM이 사라지고, 남은 COM 부분은 대화형 앱이므로 지원 구성에 들어갑니다. 전면 재작성보다 현실적이고, 위험이 높은 부분부터 순서대로 지울 수 있는 진행입니다.

8. .NET (Core) 시대의 주의점

.NET Framework에서 .NET(.NET 6/8 등)으로 이전한 앱에서 Excel COM 조작을 이어 갈 때의 주의점을, 확인된 범위에서 정리합니다.

  • COM 상호 운용은 여전히 Windows 전용입니다. .NET은 Linux에서도 동작하지만, 내장 COM 상호 운용 지원은 Windows로 한정됩니다. 12 Excel 조작을 포함한 프로젝트는 net8.0-windows처럼 타깃을 명시하고, 크로스 플랫폼을 기대하지 마십시오. Linux 컨테이너에 올릴 수 없다는 점은 7장의 교체 판단에 직결됩니다(ClosedXML이라면 Linux 컨테이너에서 동작합니다).
  • 참조 방법은 「COM 참조 + 상호 운용 형식 포함」이 기본입니다. Visual Studio에서 Microsoft Excel 개체 라이브러리를 COM 참조로 추가하면, 기본으로 상호 운용 형식 포함(Embed Interop Types)이 쓰입니다. 사용한 형식만 자기 어셈블리에 포함되므로, 실행 환경에 PIA(기본 상호 운용 어셈블리)를 배포할 필요가 없고, Office 버전 차에도 강해집니다. 13
  • dynamic과 생략 가능 인수는 계속 쓸 수 있습니다. C#의 Office 상호 운용용 기능(명명된 인수·생략 가능 인수, dynamic에 의한 COM 호출 간략화)은 현행 .NET에서도 지원됩니다. 13 다만 dynamic으로 쓰면 익명 RCW가 더 안 보이므로, 해제를 규율화하고 싶은 코드에서는 명시적 형으로 쓰기를 권합니다.
  • 플랫폼 전제 API에 주의합니다. Marshal.ReleaseComObject 같은 COM 관련 API에는 Windows 전용 특성이 붙어 있어, 크로스 플랫폼 프로젝트에서 호출하면 분석기 경고(CA1416) 대상이 됩니다. Excel 조작을 독립 프로젝트로 나누면 관리하기 쉬워집니다.
  • 반대 방향(VBA에서 .NET을 호출)은 다른 이야기입니다. .NET 8 DLL을 COM으로 공개해 VBA에서 쓰는 구성은 계속 가능하며, 절차는 「.NET 8 DLL을 VBA에서 형 지정으로 쓰는 방법」에 정리했습니다. 「C#에서 Excel을 다루는」 것이 아니라 「Excel 매크로에서 C# 로직을 호출하는」 구성으로 뒤집으면, 프로세스 수명 관리를 Excel 자신에게 맡길 수 있어 잔류 문제가 구조적으로 사라지는 경우도 있습니다.

정리하면, .NET (Core) 시대가 되어도 Excel COM 조작의 작성법과 함정은 .NET Framework 시대와 거의 같습니다. 바뀐 것은 Windows 전용임을 명시적으로 선언하는 점과, 참조가 상호 운용 형식 포함 쪽으로 기운 점이며, RCW와 해제 패턴 논의(2~5장)는 그대로 통합니다.

9. 정리

EXCEL.EXE가 남는 문제는 「COM은 참조 카운트로 살고, .NET의 RCW는 GC로 죽는다」는 두 수명 관리의 다리 놓기를 이해하면, 운에 맡긴 현상이 아니게 됩니다. 체크리스트로 압축하면 다음과 같습니다.

  • Quit()는 해제가 아니다. RCW가 붙잡은 COM 참조가 모두 돌아올 때까지 Excel은 종료하지 않는다
  • 점 두 개 이상의 복합식은 익명 RCW를 만든다. 중간 객체는 변수에 받는다
  • 해제는 「메서드 격리 + GC 3점 세트」를 기본으로 하고, 순서 제어가 필요하면 using 래퍼로 ReleaseComObject를 규율화한다. 날것의 ReleaseComObject를 흩뿌리지 않는다
  • 보험 Kill은 Application.Hwnd → GetWindowThreadProcessId로 자기 인스턴스만 특정하고, 「Quit → 대기 → 타임아웃일 때만」 발동한다
  • 서버·무인 실행의 Excel 자동화는 미지원 구성이다. 무인 장표 생성은 ClosedXML / Open XML SDK로, 매크로나 재계산이 필요한 처리는 대화형 환경의 COM으로 격리한다

작업 관리자에 EXCEL.EXE가 늘어서 있는 것을 발견하면, 그것은 기법의 문제(4~5장)이거나 구성의 문제(6~7장) 어느 쪽의 신호입니다. 손안의 코드가 어디에 해당하는지, 바꾼다면 어느 범위부터인지 판단이 망설여지면, 처리 목록을 정리하는 일부터 도와드릴 수 있습니다.

관련 기사

관련 상담 영역

合同会社小村ソフト에서는 Excel/Office 자동화를 포함한 Windows 앱의 트러블 조사(프로세스 잔류, 행, 파일 잠금), COM 자산의 유지보수와 규율화, Open XML 계열 라이브러리로의 장표 처리 이전 설계를 다룹니다.

참고 링크

  1. Microsoft Learn, Runtime Callable Wrapper. RCW가 COM 객체마다 프로세스 안에 하나 만들어지는 것, 인터페이스 포인터를 캐시하고, GC에 의해 회수될 때 COM 객체에 대한 참조를 해제하는 것에 대해. ↩ ↩2 ↩3 ↩4 ↩5

  2. Microsoft Learn, Marshal.ReleaseComObject(Object) Method. RCW 참조 카운트를 감소시키는 동작, 이미 해제한 RCW 사용으로 인한 InvalidComObjectException·액세스 위반·메모리 파괴 위험, 「반드시 필요한 경우에만 사용한다」는 명시, FinalReleaseComObject와의 관계에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7

  3. Microsoft Learn, Application.hWnd property (Excel). Excel Application 객체의 Hwnd 속성이 최상위 윈도우 핸들을 반환하는 것에 대해. ↩ ↩2 ↩3

  4. Microsoft Learn, GetWindowThreadProcessId function (winuser.h). 지정한 윈도우를 만든 스레드 ID와, 윈도우를 만든 프로세스 ID를 취득할 수 있는 것에 대해. ↩ ↩2

  5. Microsoft 지원, Considerations for server-side Automation of Office. 무인·비대화형 클라이언트 애플리케이션이나 컴포넌트(ASP, ASP.NET, DCOM, NT 서비스 포함)에서의 Office 자동화를 Microsoft가 권장하지 않으며 지원하지 않는 것에 대해. ↩ ↩2 ↩3

  6. Microsoft Learn, Welcome to the Open XML SDK for Office. Open XML SDK가 ECMA-376 / ISO/IEC 29500 표준 Office 파일 형식을 강력한 형식의 클래스로 다루는 System.IO.Packaging 기반 라이브러리인 것에 대해. ↩ ↩2

  7. GitHub, ClosedXML/ClosedXML. Open XML API 위에 직관적 인터페이스를 제공하는 MIT 라이선스 라이브러리로, Excel 설치 없이 Excel 2007+ (.xlsx, .xlsm) 파일을 다룰 수 있는 것에 대해. ↩ ↩2 ↩3

  8. Microsoft 지원, Office application does not exit after automation from Visual Studio .NET client. Quit를 호출해도 Office 앱이 종료하지 않는 원인이 RCW가 유지하는 참조라는 것, 대책으로 각 객체를 새 변수로 선언하는 것(oExcel.Workbooks를 중간 변수에 받는 예), Marshal.ReleaseComObject를 0이 돌아올 때까지 호출하는 것, 변수를 null로 두는 것, Quit를 호출하는 것, 그래도 종료하지 않으면 GC.Collect와 GC.WaitForPendingFinalizers를 호출하는 것에 대해. ↩ ↩2

  9. Microsoft Learn, _Application.AutomationSecurity Property (Microsoft.Office.Interop.Excel). 프로그램에서 파일을 열 때의 매크로 보안 모드. 애플리케이션 기동 시 기본이 msoAutomationSecurityLow(모든 매크로 사용)인 것, msoAutomationSecurityForceDisable로 경고 없이 모든 매크로를 무효화할 수 있는 것에 대해. ↩

  10. Microsoft Learn, Considerations for unattended automation of Office in the Microsoft 365 for unattended RPA environment. 무인 자동화에서의 대화형 UI·사용자 식별·STA 단일 스레드 설계 등의 문제점, 무인 라이선스 아래에서도 동작이 AS IS인 것, 대안으로 Microsoft Graph와 Open XML 파일 형식의 직접 편집이 권장되는 것에 대해. ↩ ↩2 ↩3 ↩4 ↩5

  11. Microsoft Learn, Open XML SDK for Office design considerations. Open XML SDK가 Office 개체 모델의 대체가 아니며, 수식 재계산·데이터 갱신 같은 애플리케이션 동작이나 다른 형식으로의 변환 기능을 제공하지 않는 것에 대해. ↩ ↩2 ↩3

  12. Microsoft Learn, Native interoperability ABI support. 내장 COM 상호 운용 시스템 지원이 Windows로 한정되는 것, .NET 5+의 ComWrappers / .NET 8+의 소스 생성에 의한 COM 지원에 대해. ↩

  13. Microsoft Learn, How to access Office interop objects. 명명된 인수·생략 가능 인수·dynamic에 의한 Office 상호 운용 간략화와, PIA 대신 상호 운용 형식 포함(Embed Interop Types)이 기본 동작인 것에 대해. ↩ ↩2

같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.

이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.

ActiveX 이관

COM / ActiveX / OCX 자산을 유지할지, 감쌀지, 교체할지의 단계적 판단을 정리한 토픽 페이지입니다.

이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.

자주 묻는 질문

이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.

Quit()를 호출했는데도 EXCEL.EXE가 종료되지 않는 이유는 무엇입니까?
Quit()는 해제가 아니라, 「모든 참조가 해제되면 종료해도 된다」는 요청에 지나지 않기 때문입니다. .NET은 COM 객체를 RCW(Runtime Callable Wrapper)라는 래퍼를 통해 다루며, RCW가 GC에 회수될 때까지(또는 명시적으로 해제될 때까지) COM 참조를 계속 붙잡고 있습니다. 참조가 남아 있는 한 Excel은 그대로 기다립니다. 해제 시점이 GC에 의존해 정해지지 않으므로, 「개발 머신에서는 사라지는데 운영에서는 남는다」는 재현성 없는 현상도 GC 타이밍의 흔들림으로 설명할 수 있습니다.
Excel COM 조작의 「2-dot 규칙」이란 무엇입니까?
COM 객체에 점을 두 개 이상 잇지 말고, 모든 중간 객체를 한 번 변수에 받으라는 경험 규칙입니다. book.Worksheets[1].Range["A1"] 같은 복합식은 Worksheets(컬렉션), Worksheets[1](시트), Range["A1"](Range)처럼 한 줄에서 여러 익명 RCW를 만듭니다. 이들은 변수에 들어 있지 않아 Marshal.ReleaseComObject를 호출할 수단이 없고, 해제 누수의 주원인이 됩니다. foreach나 조건식 안에서 값을 버리고 읽는 경우, 복합식을 인수로 넘기는 경우에도 마찬가지로 익명 RCW가 생긴다는 점에 주의가 필요합니다.
EXCEL.EXE를 확실히 종료시키려면 어떻게 작성하면 됩니까?
권장하는 방식은 Excel을 다루는 처리를 하나의 메서드에 완전히 격리하고, 메서드를 빠져나온 뒤에 GC.Collect → GC.WaitForPendingFinalizers → GC.Collect의 3점 세트를 실행하는 것입니다. RCW는 본래 GC로 회수될 때 COM 참조를 해제하는 구조이므로, 이 형태라면 2-dot 규칙을 깨더라도 익명 RCW까지 함께 회수됩니다. Marshal.ReleaseComObject를 모든 객체에 적용하는 방식도 있지만, 공식 문서는 「반드시 필요한 경우에만 사용한다」고 명시하고 있으며, 이미 해제한 RCW를 잘못 쓰면 InvalidComObjectException이나 메모리 파괴로 이어집니다. 순서 제어가 필요한 경우에만 using으로 규율을 맞춘 래퍼를 도입합니다.
서버나 배치에서 Excel을 자동화해도 됩니까?
Microsoft는 무인·비대화형 클라이언트 애플리케이션이나 컴포넌트(ASP.NET, DCOM, NT 서비스 포함)에서의 Office 자동화를 권장하지 않으며, 지원하지도 않는다고 명시합니다. Office는 대화형 사용자가 있다는 전제로 설계되어 있어, 서비스나 IIS 위에서는 대화 상자 대기로 인한 행이나 프로세스의 무한 적체가 일어납니다. 무인 실행의 장표 생성이라면 Excel을 기동하지 않고 파일을 직접 다루는 Open XML SDK나 ClosedXML로의 교체가 권장 대안입니다. 매크로 실행이나 재계산처럼 Excel 본체의 기능이 필요한 처리만 대화형 환경의 COM으로 격리하는 분할이 현실적입니다.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기