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

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

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

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

1. 먼저 결론

  • EXCEL.EXE가 남는 것은 버그가 아니라, .NET 쪽이 아직 COM 참조를 쥐고 있기 때문입니다. Quit()은 「모든 참조가 해제되면 종료한다」는 요청일 뿐이며, 참조가 남아 있으면 Excel은 성실하게 계속 기다립니다.
  • .NET은 COM 객체를 RCW(Runtime Callable Wrapper)라는 래퍼를 통해 다루며, RCW가 GC에 회수되거나(또는 명시적으로 해제될) 때까지 COM 객체에 대한 참조를 계속 유지합니다. 1
  • book.Worksheets[1].Range["A1"]처럼 점을 2개 이상 연결하면, 중간 객체의 RCW가 어떤 변수에도 담기지 않은 채로 생성되어 해제 누락이 됩니다. 통칭 「2닷 규칙」이라 불리며, 이 문제의 주범입니다.
  • 해제 패턴에는 2가지 유파가 있습니다. (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

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가 남는 이유를 그대로 도출할 수 있습니다.

단계 일어나는 일
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닷 규칙」 ── 보이지 않는 중간 객체

「모든 변수를 제대로 해제했는데도 남는다」는 상담으로 들어오는 코드에는, 거의 예외 없이 다음과 같은 줄이 있습니다.

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

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

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

// 모든 중간 객체에 이름을 붙인다
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의 이벤트에 핸들러를 붙이면, 그 연결이 참조를 유지합니다. 종료 전에 반드시 구독을 해지합니다.

4. 해제 패턴 정리 ── ReleaseComObject파와 GC파

EXCEL.EXE를 확실히 종료시키는 작성 방식에는 2가지 유파가 있습니다. 둘 다 올바르게 작성하면 동작합니다. 문제는 「계속 올바르게 작성할 수 있는가」이며, 여기서 실무상의 차이가 발생합니다.

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

Marshal.ReleaseComObject는 RCW의 내부 참조 카운트를 감소시키고, 0이 되는 시점에 RCW가 쥐고 있는 COM 참조를 즉시 해제합니다. 2 GC를 기다리지 않고 결정적인 시점에 해제할 수 있다는 것이 장점입니다. 모든 객체에 적용한 전형적인 형태는 다음과 같습니다.

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 })는 사용하지 않는다.
    // 세터의 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 객체를 하나도 빠짐없이 변수에 받고, 예외 경로까지 포함해 역순으로 해제해야 합니다. 게다가 CloseQuit 자체가 실패하는(COM 오류, 연결이 끊긴 워크북, 응답 없는 Excel) 경우까지 고려하면, 위와 같이 중첩된 try/finally로 「도중에 실패해도 이후의 해제에 반드시 도달한다」는 것까지 보장해야 합니다──이를 모든 팀원이 모든 수정에서 계속 지키게 하는 것은 경험상 상당히 어려운 요구입니다. 단 한 곳이라도 2닷 복합식이 섞여 들어가면 누락이 되살아납니다.

더 중요한 것은, 공식 문서 자체가 남용에 대해 경고하고 있다는 점입니다. 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한 워크북은 매크로 경고 없이 VBA가 실행될 수 있습니다. 이 글의 예제는 자사 앱이 관리하는 신뢰된 템플릿을 여는 것을 전제로 하지만, 공유 폴더의 파일이나 사용자가 업로드한 파일 등 외부에서 유래한 워크북을 열 가능성이 있다면, Open 전에 excel.AutomationSecurity = MsoAutomationSecurity.msoAutomationSecurityForceDisable(Microsoft.Office.Core 네임스페이스)을 설정해 매크로를 강제로 비활성화하십시오. 8 템플릿 교체가 그대로 임의 코드 실행으로 이어지는 유형의 공격을 막을 수 있습니다. 이 주의점은 뒤에서 설명할 GC 패턴 예제에도 그대로 적용됩니다.

4.2 (b) 참조를 가둬 GC가 회수하게 하는 파

또 다른 방식은 RCW의 해제를 본래의 구조대로 GC에 맡기고, 그 GC를 확정적인 시점에 실행하는 방식입니다. RCW는 GC에 회수될 때 COM 참조를 해제하므로1, 「Excel을 다루는 모든 참조가 스코프를 벗어난 후에 풀 GC + 파이널라이저 완료 대기를 수행」함으로써, 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();   // 파이널라이저가 분리한 RCW를 회수하는 2번째 순회
    }
}

[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();
    }
}

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

  1. Excel을 다루는 코드를 하나의 메서드에 격리할 것. JIT가 스택 위의 참조를 살려 두는 동안에는 GC가 회수할 수 없기 때문에, GC는 반드시 「Excel을 다룬 메서드 바깥」에서 호출합니다. NoInlining은 인라인 확장으로 격리가 무효화되는 것을 막기 위한 것입니다.
  2. RCW를 필드나 반환값으로 빠져나가게 하지 않을 것. 하나라도 메서드 밖으로 새어 나가면, 그 참조가 살아 있는 동안 Excel은 남습니다.
  3. GC.CollectGC.WaitForPendingFinalizersGC.Collect의 3종 세트로 만들 것. RCW의 마무리는 파이널라이저를 거쳐 이루어지므로, 첫 번째 Collect로 감지하고, 파이널라이저의 완료를 기다린 뒤, 두 번째 Collect로 잔재를 회수하는 2회 순회가 정석입니다.

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

이 방식의 장점은 2닷 규칙을 어겨도 누락되지 않는다는 것입니다. 익명의 중간 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닷 규칙에 대한 교육은 어차피 필요합니다.

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

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.HwndVisible = 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 호출 자체가 행(hang)된 경우(숨겨진 모달 대화상자나 애드인의 응답 대기)에는 finally에 도달하지 못해, 이 Kill도 작동하지 않습니다. 대책은 2단계로 생각합니다. 먼저 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의 작업자 프로세스 위에서는 하나같이 문제를 일으킵니다. 9 「Windows 서비스에서 실행했더니 운영 환경에서 Open이 반환되지 않는다」「IIS 위에서 한 달에 한 번 대화상자 대기로 행(hang)된다」와 같은 상담은 모두 이 비지원 구성이 원인이었습니다. 이 구성에서의 EXCEL.EXE 잔류는 단순한 마무리 문제가 아니라, 행된 프로세스가 무한히 누적되는 운영 문제가 됩니다.

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

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

참고로 Microsoft 365에는 무인 RPA용 라이선스(unattended license)가 존재하지만, 이는 무인 실행을 라이선스상 가능하게 하는 것일 뿐, 동작은 여전히 “AS IS”──설계 밖의 사용법에 따른 예기치 못한 동작은 앱 측에서 흡수하라는 위치 설정입니다. 9 「라이선스를 사면 지원되는 구성이 된다」는 것은 아니라는 점을 오해하는 경우가 많으니 주의하십시오.

7. 판단표 ── COM을 계속 사용할지, Open XML 계열로 교체할지

위 내용을 바탕으로 하면, 「Excel을 COM으로 계속 조작할지, 벗어날지」는 다음 3가지 선택지로 정리할 수 있습니다.

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

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

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

당사가 실제 프로젝트에서 자주 제안하는 것은, 「생성은 ClosedXML로, 도저히 Excel 본체가 필요한 처리만 COM으로 격리한다」는 분할입니다. 매일 수백 건의 보고서 생성은 ClosedXML로 서버에서 실행하고, 월간 「매크로 포함 워크북 갱신」만은 담당자의 데스크톱에서 버튼 클릭으로 실행하는 COM 처리로 남겨 둡니다──이렇게 하면 무인 환경에서 COM이 사라지고, 남은 COM 부분은 대화형 앱이므로 지원 구성에 들어맞습니다. 전면 재작성보다 현실적이며, 위험이 높은 부분부터 순서대로 제거해 나갈 수 있는 접근 방식입니다.

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

.NET Framework에서 .NET(.NET 6/8 등)으로 이전한 앱에서 Excel COM 조작을 계속하는 경우의 주의점을, 확인할 수 있는 범위에서 정리합니다.

  • COM 상호운용은 여전히 Windows 전용입니다. .NET은 Linux에서도 동작하지만, 내장된 COM 상호운용 지원은 Windows로 한정되어 있습니다. 11 Excel 조작을 포함하는 프로젝트는 net8.0-windows처럼 타겟을 명시하고, 크로스 플랫폼을 기대하지 마십시오. Linux 컨테이너에 올릴 수 없다는 점은 7장의 교체 판단과 직결됩니다(ClosedXML이라면 Linux 컨테이너에서 동작합니다).
  • 참조 방법은 「COM 참조 + 상호운용 형식 임베드」가 기본입니다. Visual Studio에서 Microsoft Excel 개체 라이브러리를 COM 참조로 추가하면, 기본적으로 상호운용 형식 임베드(Embed Interop Types)가 사용됩니다. 사용한 형식만 자신의 어셈블리에 임베드되기 때문에, 실행 환경에 PIA(주 상호운용 어셈블리)를 배포할 필요가 없고, Office의 버전 차이에도 강해집니다. 12
  • dynamic과 생략 가능 인수는 계속 사용할 수 있습니다. C#의 Office 상호운용용 기능(명명된 인수・생략 가능 인수, dynamic을 통한 COM 호출 간소화)은 현재의 .NET에서도 지원됩니다. 12 다만 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은 종료되지 않는다
  • 점 2개 이상의 복합식은 익명 RCW를 만든다. 중간 객체는 변수에 받는다
  • 해제는 「메서드 격리 + GC 3종 세트」를 기본으로 하고, 순서 제어가 필요하면 using 래퍼로 ReleaseComObject를 규율화한다. 원시 ReleaseComObject를 곳곳에 흩어 놓지 않는다
  • 보험용 Kill은 Application.HwndGetWindowThreadProcessId로 자신의 인스턴스만 특정하여, 「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

  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 Learn, _Application.AutomationSecurity Property (Microsoft.Office.Interop.Excel). 프로그램에서 파일을 열 때의 매크로 보안 모드. 애플리케이션 시작 시의 기본값이 msoAutomationSecurityLow(모든 매크로 활성)라는 것, msoAutomationSecurityForceDisable로 경고 없이 모든 매크로를 비활성화할 수 있다는 것에 대해. 

  9. 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

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

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

  12. 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에 의존해 불확정적이기 때문에, 「개발 PC에서는 사라지는데 운영 환경에서는 남는다」는 재현성 없는 현상도 GC 타이밍의 흔들림으로 설명할 수 있습니다.
Excel COM 조작의 「2닷 규칙」이란 무엇인가요?
COM 객체에 점(.)을 두 개 이상 연결하지 않고, 모든 중간 객체를 한 번 변수에 받는다는 경험 법칙입니다. book.Worksheets[1].Range["A1"]과 같은 복합식은 Worksheets(컬렉션), Worksheets[1](시트), Range["A1"](범위)처럼 한 줄에서 여러 개의 익명 RCW를 생성합니다. 이들은 변수에 담기지 않아 Marshal.ReleaseComObject를 호출할 방법이 없어 해제 누락의 주범이 됩니다. foreach나 조건식 안에서 버려지는 값, 복합식의 인수에서도 마찬가지로 익명 RCW가 생기는 점에 주의해야 합니다.
EXCEL.EXE를 확실히 종료시키려면 어떻게 작성해야 하나요?
권장되는 방법은 Excel을 다루는 처리를 하나의 메서드에 완전히 격리하고, 메서드를 벗어난 후 GC.Collect → GC.WaitForPendingFinalizers → GC.Collect의 3종 세트를 실행하는 방식입니다. RCW는 원래 GC에 회수될 때 COM 참조를 해제하는 구조이므로, 이 형태라면 2닷 규칙을 어겨도 익명 RCW까지 함께 회수됩니다. Marshal.ReleaseComObject를 모든 객체에 적용하는 방식도 있지만, 공식 문서는 「반드시 필요한 경우에만 사용할 것」이라고 명시하고 있으며, 해제된 RCW를 잘못 사용하면 InvalidComObjectException이나 메모리 손상으로 이어집니다. 해제 순서 제어가 필요한 경우에만 using으로 규율화한 래퍼를 도입합니다.
서버나 배치 작업에서 Excel을 자동화해도 되나요?
Microsoft는 무인·비대화형 클라이언트 애플리케이션이나 구성 요소(ASP.NET, DCOM, NT 서비스 포함)에서의 Office 자동화를 권장하지 않으며 지원하지도 않는다고 명확히 밝히고 있습니다. Office는 대화형 사용자의 존재를 전제로 설계되어 있어, 서비스나 IIS 위에서는 대화상자 대기로 인한 행(hang)이나 프로세스의 무한 누적이 발생합니다. 무인 실행의 보고서 생성이라면 Excel을 실행하지 않고 파일을 직접 다루는 Open XML SDK나 ClosedXML로 교체하는 것이 권장 대안입니다. 매크로 실행이나 재계산 등 Excel 본체의 기능이 필요한 처리만 대화형 환경의 COM으로 격리하는 분리가 현실적입니다.

저자 프로필

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

Go Komura

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

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

블로그 목록으로 돌아가기