「코맨드릿을 찾아봤지만 하고 싶은 처리가 없다」── PowerShell을 어느 정도 사용하다 보면 반드시 이 벽에 부딪힙니다. 바로가기 일괄 생성, ZIP 내용물을 압축 해제하지 않고 확인하는 처리, 창 조작, Excel 파일 읽기・쓰기. 표준 코맨드릿만으로는 손이 닿지 않는 요청은 중소기업의 정보시스템·운영 담당자에게서 일상적으로 상담이 들어옵니다.
사실 이 벽 너머는 PowerShell이 잘하는 영역입니다. PowerShell은 .NET 위에 구축되어 있기 때문에 .NET 클래스 라이브러리의 타입을 추가 설치 없이 직접 호출할 수 있습니다. 게다가 Add-Type으로 C# 코드나 Win32 API(P/Invoke)를 그 자리에서 편입할 수 있고, New-Object -ComObject로 WScript.Shell이나 Excel 등의 COM 객체도 조작할 수 있습니다. VBScript나 VBA로 작성되어 온 “Windows 자동화” 도구 상자가 거의 그대로 사용 가능하다는 뜻입니다.
다만 이 힘에는 뒷정리 작법이 따라붙습니다. 특히 Excel의 COM 조작은 “스크립트가 끝나도 EXCEL.EXE가 남는다”라는 단골 문제의 온상이며, 애초에 무인 실행으로 Office를 자동화하는 것 자체를 Microsoft가 비권장으로 명시하고 있습니다. 이 글에서는 PowerShell에서의 .NET 호출·Add-Type·COM 조작의 실무 패턴과 함정, 그리고 “어디까지 스크립트로 버티고, 어디서부터 C# 도구화할지”의 판단까지 정리합니다.
1. 먼저 결론
- PowerShell은 .NET 위에 올라가 있습니다. Windows PowerShell 5.1은 .NET Framework, PowerShell 7은 .NET(구 .NET Core) 위에 구축되어 있으며,
[System.IO.Path]::GetFileNameWithoutExtension()처럼 .NET의 정적 메서드를 직접 호출할 수 있습니다.1 - 인스턴스 생성은
New-Object나, PowerShell 5.0 이후라면[타입]::new()입니다.[타입]::new라고 (괄호 없이) 입력하면 생성자 목록을 확인할 수 있어, 인수를 조사하며 작성할 수 있습니다.2 Add-Type을 사용하면 C# 소스 코드를 그 자리에서 컴파일해 세션에 편입할 수 있습니다. DllImport 시그니처를 전달하면 Win32 API의 P/Invoke 호출도 가능합니다. 추가한 타입은 세션 내에서 삭제할 수 없고, 같은 이름의 타입은 다시 정의할 수 없습니다.3- COM 객체는
New-Object -ComObject <ProgId>로 생성합니다. VBScript의CreateObject("Shell.Application")은New-Object -ComObject Shell.Application에 그대로 대응하며, WScript.Shell을 이용한 바로가기 생성 등 WSH 시대의 도구가 PowerShell에서 사용 가능합니다.45 - COM 객체의 수명은 참조 카운트로 관리되며, .NET 쪽에서는 RCW(Runtime Callable Wrapper)가 이를 쥐고 있습니다. 참조가 남아 있으면 Excel 등의 프로세스는 종료되지 않습니다. 명시적으로 해제하려면
Marshal.ReleaseComObject를 사용하지만, 남용하면 다른 사고를 부르기 때문에 “정말 필요한 경우에만”이 공식 지침입니다.67 - 무인 실행에서의 Office 자동화는 Microsoft가 “권장하지 않으며 지원하지도 않는다”고 명시하고 있습니다. 서비스·작업 스케줄러·서버 사이드에서의 Excel COM 조작은 동작하는 것처럼 보여도 비지원 구성입니다. 무인 처리는 Open XML 계열 라이브러리나 CSV 연계로 대체합니다.8
- 5.1과 7은 사용할 수 있는 타입・메서드가 다릅니다. String.Split의 오버로드 차이처럼 같은 코드라도 동작이 달라지는 예가 있으며, 5.1에서는 GAC의 어셈블리를
Add-Type으로 명시적으로 로드해야 하는 경우도 있습니다.13 - .NET 직접 호출이 늘어나기 시작하면 C# 도구화를 검토할 시점입니다. 타입을 재정의할 수 없는 Add-Type의 제약이나 배포 문제가 눈에 띄기 시작하면 스크립트의 한계가 가까워졌다는 신호입니다.
2. PowerShell은 .NET 위에 올라가 있다 ── [타입]::메서드와 New-Object
PowerShell의 코맨드릿은 .NET 클래스 라이브러리의 일부를 “운영 작업에 쓰기 쉽게 감싼 것”입니다. 감싸이지 않은 기능도 타입 이름을 대괄호로 쓰면 직접 호출할 수 있습니다. 정적 메서드는 ::, 인스턴스의 메서드나 속성은 .입니다.
# .NET의 정적 메서드를 직접 호출한다. 코맨드릿 불필요, 추가 설치도 불필요
[System.IO.Path]::GetFileNameWithoutExtension('C:\data\受注_20260718.csv') # -> 受注_20260718
[System.IO.Path]::Combine('C:\data', 'archive', '2026-07') # -> C:\data\archive\2026-07
[System.Math]::Round(123.456, 1) # -> 123.5
# 인스턴스가 필요하면 New-Object나 [타입]::new()로 만든다
$list = [System.Collections.Generic.List[string]]::new()
$list.Add('server01')
# ::new를 괄호 없이 입력하면 생성자 목록이 반환된다(인수 조사 방법으로 편리)
[System.IO.StreamWriter]::new
[타입]::new()는 PowerShell 5.0에서 추가된 표기법으로, New-Object보다 빠른 데다 위 예시처럼 생성자의 오버로드 목록을 그 자리에서 확인할 수 있습니다.2 한편 주의점도 있습니다. Get-Item 등의 코맨드릿이 반환하는 객체에는 PowerShell이 추가 속성(NoteProperty)을 붙이는 경우가 있어, ::new()로 직접 만든 같은 타입의 객체와는 멤버 구성이 일치하지 않는 경우가 있습니다.2 “코맨드릿을 거쳐 가져왔을 때는 있던 속성이 없다”고 혼란스럽다면 이 구조를 떠올리세요.
실무 레시피를 하나 소개합니다. ZIP 처리는 Compress-Archive / Expand-Archive가 있지만, 이들의 기반인 ZipArchive API에는 파일 크기 2GB 제한이 있습니다. 이 제한은 압축·압축 해제 양쪽의 공식 문서에 기재되어 있으며, 둘 다 “기반이 되는 System.IO.Compression.ZipArchive API에 의한 상한”이라고 명기한 뒤 해당 클래스의 레퍼런스를 참조처로 들고 있습니다.910 또한 “압축을 풀지 않고 내용물 목록만 확인하고 싶다” 같은 세세한 조작은 코맨드릿에 없습니다. 여기서 .NET의 ZipFile 클래스가 나설 차례입니다.11
# Windows PowerShell 5.1에서는 GAC의 어셈블리를 명시적으로 로드해야 한다
# (PowerShell 7에서는 동봉 어셈블리가 필요 시 자동으로 로드되므로 이 줄은 없어도 동작한다)
Add-Type -AssemblyName System.IO.Compression.FileSystem
# 먼저 읽기부터 ── 압축을 풀지 않고 ZIP 내용물을 확인한다
$archive = [System.IO.Compression.ZipFile]::OpenRead('C:\deploy\release.zip')
try {
$archive.Entries | Select-Object FullName, Length, LastWriteTime
}
finally {
$archive.Dispose() # 파일 핸들을 확실히 반환한다
}
# 내용에 문제가 없으면 압축을 푼다. 압축 해제 대상에 동일한 이름의 파일이 있으면
# 이 2인수 버전은 도중까지 압축을 푼 상태에서 예외를 낸다(덮어쓰지 않는다). 기존 폴더로의
# 직접 압축 해제는 피하고, 매번 새 폴더에 압축을 푼 뒤 전환하는 것이 안전하다
$dest = "C:\apps\web_$(Get-Date -Format yyyyMMddHHmmss)"
[System.IO.Compression.ZipFile]::ExtractToDirectory('C:\deploy\release.zip', $dest)
.NET Framework에서는 ZipFile 클래스를 사용하는 데 System.IO.Compression.FileSystem 어셈블리 참조가 필요하며, 이는 PowerShell 5.1에서도 마찬가지입니다.11 7에서는 동봉 어셈블리가 요청 시 자동으로 로드되므로 Add-Type 없이도 동작하지만, 써 두면 양쪽에서 동작하므로 공용 스크립트에서는 명시하는 것이 무난합니다.3
3. Add-Type으로 자체 C#과 Win32 API(P/Invoke)를 편입한다
.NET에 있는 기능 다음은 .NET에 없는 기능입니다. Add-Type에 C# 소스 코드를 전달하면 그 자리에서 컴파일되어 세션에 타입이 추가됩니다. 게다가 -MemberDefinition에 DllImport가 붙은 시그니처를 전달하면 Win32 API를 P/Invoke로 호출할 수 있습니다. 이 사용법은 공식 문서에도 예시로 실려 있는 정당한 수단입니다.3
# 로컬에서 실행한 장시간 처리의 완료를 대화상자로 알린다(user32.dll의 MessageBoxW를 P/Invoke로 호출)
$signature = @'
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern int MessageBoxW(IntPtr hWnd, string text, string caption, uint type);
'@
$native = Add-Type -MemberDefinition $signature -Name 'NativeMethods' `
-Namespace 'Win32' -PassThru
# MB_ICONINFORMATION(0x40)을 붙여서 표시
[void]$native::MessageBoxW([IntPtr]::Zero, '백업 처리가 완료되었습니다.', '장시간 처리', 0x40)
한 가지 주의할 점이 있습니다. 모달 대화상자는 자신이 데스크톱 앞에 있는 대화형 실행 전용입니다. 작업 스케줄러의 무인 실행이나 Windows 서비스 같은 비대화형 세션에서는, 아무도 닫을 수 없는 대화상자에 걸려 처리가 멈춘 채 돌아오지 않게 됩니다. 무인 작업의 알림은 로그 파일·이벤트 로그·메일 전송 등 응답을 기다리지 않는 수단으로 하세요.
같은 “처리 완료 알림”을 무인 작업용으로 다시 쓰면 이렇게 됩니다. 대화상자 대신 이벤트 로그에 한 건 기록하고, 아울러 로그 파일에도 남기는 형태입니다.
# 무인 작업용 알림: 응답을 기다리지 않고, 나중에 추적할 수 있는 곳에 남긴다
$source = 'KomuraSoft.NightlyJob' # 이벤트 소스 이름(임의의 문자열)
$logPath = 'C:\logs\nightly.log'
# 소스 등록에는 관리자 권한이 필요하다. 도입 시 한 번만 실행해 두고,
# 야간 작업 본체에서는 등록이 끝났다는 전제로 기록만 한다
# New-EventLog -LogName Application -Source 'KomuraSoft.NightlyJob'
Write-EventLog -LogName Application -Source $source -EntryType Information `
-EventId 1000 -Message '백업 처리가 완료되었습니다.'
Add-Content -LiteralPath $logPath -Value "$(Get-Date -Format o) 백업 처리가 완료되었습니다."
이벤트 뷰어 > Windows 로그 > Application에서 소스 이름 KomuraSoft.NightlyJob으로 필터링하면 이력을 추적할 수 있습니다. 모니터링 도구를 도입한 환경이라면 이 이벤트 ID를 잡아서 알림으로 돌릴 수 있습니다. 참고로 New-EventLog / Write-EventLog는 Windows PowerShell 5.1의 코맨드릿이며, PowerShell 7에서는 삭제되었습니다(6장의 표 참조). PowerShell 7에서 실행하는 야간 작업이라면 알림 부분만 powershell.exe에 넘기거나, 로그 파일 기록과 파일 감시 쪽에서의 알림(작업 스케줄러의 이벤트 트리거나 Power Automate 등)으로 옮기는 것이 자연스럽습니다.
Add-Type에는 운영상 중요한 제약이 세 가지 있습니다.3
- 추가한 타입은 그 세션에서만 유효합니다. 다른 세션이나 원격에서는 다시
Add-Type이 필요합니다. - 같은 이름의 타입은 다시 정의할 수 없습니다. 시그니처를 잘못 써서 수정하고 싶을 때는 이름을 바꾸거나 새 세션을 시작합니다. 시행착오는 일회용 콘솔에서 하세요.
- PowerShell 7에서는 같은 이름의 타입이 이미 존재하면 컴파일 자체를 건너뜁니다. “고쳤을 코드가 반영되지 않는다”면 이를 의심합니다.
한 가지 더, P/Invoke 공통 주의사항입니다. 시그니처(인수 타입, 문자 집합, 호출 규약)를 잘못 쓰면 예외로 끝나지 않고 프로세스째 다운될 수 있습니다. 문자열이나 핸들 마샬링의 함정은 「C#에서 Win32 API를 안전하게 호출하기 — P/Invoke 실무 가이드(DllImport / LibraryImport / CsWin32)」에서 자세히 정리했으니, Add-Type으로 본격적으로 Win32 API를 사용하기 전에 한 번 읽어보시길 권합니다.
4. COM을 호출한다 ── New-Object -ComObject와 WSH 도구 상자
.NET보다 오래전부터 Windows에 실려 있는 부품군이 COM입니다. New-Object -ComObject <ProgId>로 생성할 수 있고, VBScript의 Set objShell = CreateObject("Shell.Application")은 $objShell = New-Object -ComObject Shell.Application에 그대로 대응합니다.4 WScript.Shell, WScript.Network, Scripting.FileSystemObject 같은 WSH(Windows Script Host) 유래 객체도 마찬가지로 사용할 수 있습니다.5 COM이 무엇인가라는 기초 이야기는 「COM / ActiveX / OCX란 무엇인가 - 차이와 관계를 정리해서 해설」을 참조하세요.
대표적인 실무 레시피가 바로가기 일괄 생성입니다. 바로가기(.lnk) 생성은 코맨드릿이 존재하지 않으며, 공식 문서도 “바로가기 생성 같은 일부 작업은 WSH 클래스를 사용하는 편이 쉽다”며 WScript.Shell의 예시를 싣고 있습니다.5
# 공유 폴더의 도구로 향하는 바로가기를 모든 단말의 공용 바탕화면에 배포하는 상황
$shortcuts = Import-Csv 'C:\deploy\shortcuts.csv' # Name, Target, Args의 3열
$wsh = New-Object -ComObject WScript.Shell
foreach ($item in $shortcuts) {
$lnkPath = Join-Path 'C:\Users\Public\Desktop' "$($item.Name).lnk"
$lnk = $wsh.CreateShortcut($lnkPath) # 기존 .lnk가 있으면 덮어쓰기 대상이 된다
$lnk.TargetPath = $item.Target
$lnk.Arguments = $item.Args
$lnk.Save()
Write-Host "생성: $lnkPath"
}
COM 객체의 멤버는 $wsh | Get-Member로 조사할 수 있습니다.5 Outlook에서의 메일 초안 작성, Shell.Application에서의 특수 폴더 조작 등 응용 범위는 방대합니다. 다만 Excel.Application 같은 ActiveX 실행 파일(별도 프로세스로 실행되는 COM 서버)에는 다음 장의 뒷정리 문제가 따라붙습니다. 공식 문서도 참조를 놓았을 때 프로세스가 종료되는지 여부는 상대 애플리케이션에 달려 있으므로 사용 전에 종료 동작을 테스트해야 한다고 주의를 주고 있습니다.5
5. COM의 뒷정리 ── EXCEL.EXE가 남는 문제와 “무인 Office는 비지원”
5.1. 왜 프로세스가 남는가
COM 객체의 수명은 참조 카운트로 관리됩니다. .NET(=PowerShell)에서 COM을 만지면 COM 객체마다 RCW(Runtime Callable Wrapper)라는 프록시가 만들어지고, RCW가 살아 있는 한 COM 쪽 참조는 해제되지 않습니다. RCW의 회수는 가비지 컬렉션에 맡겨집니다.6 즉 $excel.Quit()을 호출해도, 어딘가에 참조(그것도 $excel.Workbooks.Open(...)처럼 변수에 담지 않은 중간 객체의 RCW를 포함해서)가 남아 있으면 EXCEL.EXE는 종료되지 않습니다.
다음 코드는 try/finally가 삼중으로 중첩되어 있어, 처음 보면 구조를 읽기 어려울 것입니다. 먼저 왜 이렇게 쓰는지를 짚어 두겠습니다. 요컨대 “나중 처리일수록 절대 건너뛰고 싶지 않다”는 것이므로, 건너뛰고 싶지 않은 순서대로 안쪽에 넣고 있을 뿐입니다.
- 가장 바깥쪽
try: Excel을 다루는 본체 처리. 여기서 무엇이 실패하든 이후의 뒷정리에는 반드시 들어간다. - 1단계
finally(북을 닫는다): 저장에 실패해도 열려 있는 북은 닫으러 간다. 다만Close자체가 실패할 수도 있다. - 2단계
finally(Quit한다): 그래서Close의 실패에 휘말리지 않는 위치에서Quit을 호출한다. Excel이 응답 없음 상태가 되면Quit도 실패할 수 있다. - 3단계
finally(RCW를 해제하고 GC를 돌린다): 그래서Quit의 성패와 무관하게 참조 해제만은 반드시 실행한다. 여기가 건너뛰어지면 프로세스가 남는다.
즉 삼중인 것은 형식미가 아니라, “뒷정리 자체가 실패할 수 있다”는 전제를 한 단계씩 무너뜨린 결과입니다. 반대로 말하면 Close도 Quit도 실패하지 않는다는 전제로 써도 된다면 finally는 하나로 충분합니다.
$excel = New-Object -ComObject Excel.Application
try {
$excel.DisplayAlerts = $false
$books = $excel.Workbooks # 중간 객체도 변수로 받아, 나중에 해제할 수 있게 한다
$book = $books.Open('C:\work\월별매출.xlsx')
$sheet = $book.Worksheets.Item(1)
$sheet.Cells.Item(1, 1).Value2 = "갱신: $(Get-Date -Format 'yyyy-MM-dd')"
$book.Save()
}
finally {
# Close가 실패하는 경로야말로 EXCEL.EXE가 남기 쉬우므로,
# 중첩된 finally로 Quit과 해제를 반드시 실행한다
try {
if ($book) { $book.Close($false) }
}
finally {
# Quit이 실패해도(Excel이 응답 없음・COM 서버 절단 등) RCW 해제는 반드시 수행한다
try {
if ($excel) { $excel.Quit() }
}
finally {
# RCW의 참조 카운트를 명시적으로 줄인다(자식에서 부모 순)
foreach ($obj in @($sheet, $book, $books, $excel)) {
if ($obj) { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($obj) }
}
# 변수로 받지 못한 RCW를 GC가 회수하도록 하는 보험
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
[System.GC]::Collect()
}
}
}
Marshal.ReleaseComObject는 RCW의 참조 카운트를 줄여, 0이 된 시점에 COM 쪽 참조를 해제합니다. 다만 공식 문서는 해제된 RCW에 대한 접근이 예외나 액세스 위반을 부르기 때문에 “절대적으로 필요한 경우에만 사용할 것”이라고 강하게 주의를 주고 있습니다.7
위 코드에서 중간 객체를 하나씩 변수로 받고 있는 것은 속칭 “2닷 규칙”이라 불리는 작법입니다. $excel.Workbooks.Open(...)처럼 점을 두 개 이상 이으면 도중의 Workbooks에 대응하는 RCW가 어느 변수에도 담기지 않은 채 생성되어, 해제할 수단이 남지 않습니다. 그래서 한 줄에 점은 하나까지로 하고, 사이의 객체를 반드시 변수로 받는다 ── 는 것이 이 규칙의 내용입니다. 해제 패턴의 유파(ReleaseComObject파와 GC파), 2닷 규칙의 상세, 그래도 남은 프로세스를 특정하는 방법은 「C#의 Excel 조작에서 EXCEL.EXE가 남는 문제 ── COM 참조 해제 패턴과 교체 판단」에서 자세히 다루고 있습니다. C# 기사이지만 RCW의 구조는 PowerShell에서도 동일합니다.
5.2. 애초에 무인 실행에서 Office를 쓰지 않는다
더 근본적인 판단이 있습니다. Microsoft는 “무인・비대화형 클라이언트 애플리케이션이나 구성 요소(ASP, ASP.NET, DCOM, NT 서비스 포함)에서의 Office 자동화를, 현재 권장하지 않으며 지원도 하지 않는다”고 공식적으로 명시하고 있습니다. Office는 대화형 사용을 전제로 설계되어 있어, 무인 환경에서는 불안정한 동작이나 데드락을 보일 수 있기 때문입니다.8 예기치 않은 대화상자로 처리가 멈춘다, STA 기반이라 다중 실행을 견디지 못한다 같은 구체적인 문제도 열거되어 있습니다.8 여기서 말하는 STA는 단일 스레드 아파트먼트(Single-Threaded Apartment)의 약자로, COM의 스레드 모델 중 하나입니다. 객체에 대한 호출을 그 객체를 만든 스레드에 집중시켜 한 번에 하나씩 처리하는 구조로, Office 애플리케이션은 이 전제 위에 만들어져 있습니다. 그래서 “한 대의 서버에서 같은 처리를 여러 개 병렬로 돌린다”는 사용 방식에는 구조적으로 맞지 않는다는 이야기입니다. COM의 스레드 모델 자체는 「COM STA/MTA 기초 - 스레드 모델과 행(hang)을 피하는 사고방식」에서 자세히 다루고 있습니다.
즉 작업 스케줄러의 야간 작업이나 서비스에서 Excel COM을 두드리는 구성은, 동작하고 있는 동안에도 비지원입니다. 무인 처리에서는 Office 소프트웨어 자체를 실행하지 않는 Open XML 계열 파일 직접 편집이나 CSV 연계로의 대체가 권장됩니다.8 PowerShell에서의 구체적인 대체 레시피는 동시 공개된 「PowerShell을 이용한 Excel・CSV 업무 처리 레시피」에 정리했습니다. Excel COM을 사용해도 되는 것은 “사람이 로그온한 데스크톱에서 사람의 작업을 보조한다”는 용도까지로 선을 그으세요.
6. 5.1과 7에서 “사용할 수 있는 .NET”은 다르다
Windows PowerShell 5.1은 .NET Framework 4.5계열, PowerShell 7은 .NET(구 .NET Core) 위에 구축되어 있습니다.1 코맨드릿만 사용하는 동안에는 차이를 의식할 상황이 제한적이지만, 이 글처럼 .NET을 직접 호출하기 시작하면 차이가 표면화됩니다.
| 쟁점 | Windows PowerShell 5.1 | PowerShell 7 |
|---|---|---|
| 기반 | .NET Framework 4.5계열1 | .NET(7.4는 .NET 8.0 등, 버전마다 갱신)1 |
| 메서드 오버로드 | 적음(예: String.Split은 6종)1 | 많음(같은 Split('pq')라도 결과가 달라지는 예가 공식적으로 소개됨)1 |
| 어셈블리 로드 | GAC의 어셈블리는 Add-Type으로 명시적 로드가 필요한 경우가 많음3 |
동봉 어셈블리는 요청 시 자동 로드3 |
| 사용할 수 없게 된 것 | ─ | *-EventLog 등 Windows 전용 코맨드릿 일부가 삭제됨1 |
“5.1에서는 동작했는데 7에서는 동작하지 않는다”, “그 반대”의 어느 쪽이든 일어날 수 있습니다. 양쪽 모두 대응해야 하는 스크립트는 반드시 두 환경 모두에서 테스트하세요. 이행 판단의 전체상은 동시 공개된 「Windows PowerShell 5.1과 PowerShell 7의 차이와 이행」에서 다룹니다.
7. 실무의 정석(판단표)
| 쟁점 | 선택지(권장에 「권장」을 명기) | 판단 기준 |
|---|---|---|
| 하고 싶은 처리가 코맨드릿에 없다 | 포기한다 / .NET 클래스를 직접 호출한다(권장) | 먼저 [타입]::메서드를 찾는다. System.IO, System.Text, System.IO.Compression 부근에서 대부분의 “한 걸음 더”가 채워진다1 |
| 인스턴스 생성 | New-Object / [타입]::new()(권장) | 5.0 이후만이면 ::new(). 생성자 목록이 보이고 빠르다. COM만은 New-Object -ComObject가 유일한 선택지24 |
| Win32 API가 필요하다 | 수작업으로 참는다 / Add-Type으로 P/Invoke(권장) | 몇 개 정도의 API라면 Add-Type으로 충분하다. 시그니처 실수는 프로세스째 다운시키므로 일회용 세션에서 검증3 |
| 바로가기 생성 등 WSH 유래 조작 | COM(WScript.Shell)(권장) | 코맨드릿이 없는 영역은 COM이 지금도 현역이다. 공식 샘플도 이 방식5 |
| 사람의 작업을 보조하는 Excel 조작 | COM + 확실한 뒷정리(권장) | finally에서 Quit・ReleaseComObject・GC까지를 세트로. 중간 객체도 변수로 받는다76 |
| 무인 작업에서의 Excel/Office 처리 | COM / Office를 실행하지 않는 방식(권장) | 무인 Office 자동화는 비지원. Open XML 계열・CSV로 대체한다8 |
| 스크립트가 지나치게 커졌다 | PowerShell로 버틴다 / C# 도구화(권장) | Add-Type의 코드가 수백 줄이 되었다, 타입 재정의 제약이 개발을 방해한다, 배포처마다 매번 컴파일시키고 싶지 않다 ── 이 중 하나라도 나오면 이행 신호 |
마지막 행이 이 글을 마무리하는 판단입니다. Add-Type으로 심어 넣는 C#이 비대해지고 있다면, 그것은 이미 “C# 프로그램을 PowerShell 껍질로 감싼” 상태입니다. Visual Studio의 타입 검사와 디버거, NuGet 패키지, 단위 테스트를 사용할 수 있는 C# 프로젝트로 옮기는 편이 개발도 유지보수도 빨라집니다. C# 쪽에서 PowerShell 자산을 다시 불러오는 방법도 있으므로, 이행이 전면 재작성인 것은 아닙니다. 「C#(CSharp)에서 PowerShell을 실행하고 결과를 객체로 받는 방법」에서 다리를 놓는 패턴을 소개하고 있습니다.
8. 정리
- PowerShell은 .NET 위에 구축되어 있으며,
[타입]::정적 메서드・New-Object・[타입]::new()로 .NET 클래스 라이브러리를 직접 호출할 수 있습니다. 코맨드릿에 없는 기능의 첫 번째 후보는 .NET입니다. Add-Type으로 C# 코드를 그 자리에서 컴파일할 수 있고, DllImport 시그니처를 전달하면 Win32 API도 P/Invoke로 호출할 수 있습니다. 타입은 세션 한정이며, 같은 이름의 재정의는 불가능합니다.- COM은
New-Object -ComObject로 조작합니다. 바로가기 생성 등 WSH 유래 도구는 지금도 현역입니다. - COM의 수명은 참조 카운트+RCW로 관리되며, 참조가 남으면 EXCEL.EXE 등의 프로세스가 잔류합니다. finally에서 Quit・ReleaseComObject・GC까지 포함한 뒷정리를 작성합니다.
- 무인 실행에서의 Office 자동화는 Microsoft가 비권장・비지원이라고 명시하고 있습니다. 야간 작업은 Office를 실행하지 않는 방식으로 대체합니다.
- 5.1과 7은 기반이 되는 .NET이 달라, 사용할 수 있는 타입・오버로드가 다릅니다. 양쪽 대응 스크립트는 양쪽에서의 테스트가 필수입니다.
- Add-Type이 비대해지면 C# 도구화의 신호입니다. 스크립트와 실행 파일을 잇는 기존 타입이 있습니다.
관련 글
- C#의 Excel 조작에서 EXCEL.EXE가 남는 문제 ── COM 참조 해제 패턴과 교체 판단
- C#에서 Win32 API를 안전하게 호출하기 — P/Invoke 실무 가이드(DllImport / LibraryImport / CsWin32)
- COM / ActiveX / OCX란 무엇인가 - 차이와 관계를 정리해서 해설
- COM STA/MTA 기초 - 스레드 모델과 행(hang)을 피하는 사고방식
- C#(CSharp)에서 PowerShell을 실행하고 결과를 객체로 받는 방법
- PowerShell로 Excel・CSV 업무 처리를 자동화한다 ── 집계・대조・장표 출력의 실무 레시피
- Windows PowerShell 5.1과 PowerShell 7의 차이 ── 사내 스크립트 이행 실무 가이드
관련 상담 영역
합동회사 코무라소프트에서는 PowerShell을 이용한 사내 업무 자동화, COM 자산(Excel 연계・레거시 부품)을 포함한 스크립트 설계・개수, “스크립트가 지나치게 커진” 상태에서의 C# 도구화를 다루고 있습니다. EXCEL.EXE 잔류 같은 결함 조사도 대응합니다.
참고 링크
-
Microsoft Learn, Differences between Windows PowerShell 5.1 and PowerShell 7.x. Windows PowerShell 5.1이 .NET Framework 4.5 위에, PowerShell 6.0 이후가 .NET Core(현 .NET) 위에 구축되어 있다는 것, 각 버전의 기반이 되는 .NET 버전 목록, String.Split의 오버로드 차이로 인해 같은 코드의 결과가 달라지는 예, 7에서 삭제된 코맨드릿에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
Microsoft Learn, about_Object_Creation. PowerShell 5.0에서 모든 .NET 타입에 추가된 정적 new() 메서드, ::new라고 입력하면 생성자의 오버로드 목록을 확인할 수 있다는 것, 코맨드릿을 통해 가져온 객체에는 PowerShell이 NoteProperty를 추가하기 때문에 ::new()로 만든 객체와 멤버가 일치하지 않는 경우가 있다는 것에 대해. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Add-Type. Add-Type이 C# 소스를 컴파일하여 세션에 타입을 추가한다는 것, MemberDefinition을 통한 P/Invoke(user32.dll의 ShowWindowAsync)의 공식 샘플, 추가한 타입은 세션 한정이며 같은 이름의 타입은 재정의할 수 없다는 것, PowerShell 7에서는 같은 이름의 타입이 존재하면 컴파일을 건너뛴다는 것, 5.1에서는 GAC의 어셈블리 로드에 Add-Type이 필요하고 6 이후는 자동으로 로드된다는 것에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
Microsoft Learn, New-Object. New-Object가 .NET 또는 COM 객체의 인스턴스를 생성한다는 것, -ComObject 매개변수에 ProgId를 지정한다는 것, VBScript의 CreateObject(“Shell.Application”)이 New-Object -ComObject “Shell.Application”에 대응한다는 것에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, Creating .NET and COM objects (New-Object). WScript.Shell 등 WSH 객체의 생성, 바로가기 생성은 WSH 클래스를 사용하는 편이 쉽다는 기술과 CreateShortcut의 실제 예, COM 객체에 대한 Get-Member 적용, ActiveX 실행 파일의 종료 동작이 애플리케이션에 따라 다르므로 사전 테스트가 필요하다는 것, New-Object가 .NET의 RCW를 사용한다는 것에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, Runtime Callable Wrapper. .NET이 COM 객체를 RCW라는 프록시를 통해 공개한다는 것, COM 객체마다 하나의 RCW가 만들어진다는 것, RCW가 가비지 컬렉션으로 회수될 때 COM 객체에 대한 참조를 해제한다는 것에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, Marshal.ReleaseComObject(Object) Method. ReleaseComObject가 RCW의 참조 카운트를 줄여 0이 되면 기반 COM 객체를 해제한다는 것, 해제 후의 RCW 사용이 예외나 액세스 위반을 부른다는 것, “절대적으로 필요한 경우에만 사용”이라는 공식 주의사항, FinalReleaseComObject와의 관계에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, Considerations for unattended automation of Office in the Microsoft 365 for unattended RPA environment. Microsoft가 무인・비대화형 클라이언트 애플리케이션이나 구성 요소(ASP, ASP.NET, DCOM, NT 서비스 포함)에서의 Office 자동화를 권장하지 않으며 지원도 하지 않는다는 것, 무인 환경에서의 불안정한 동작이나 데드락 가능성, 대화상자로 인한 정지・STA 유래의 다중 실행 제약 등 구체적인 문제, Open XML 파일 형식의 직접 편집이 권장 대안이라는 것에 대해. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Expand-Archive. Expand-Archive가 System.IO.Compression.ZipArchive API를 사용한다는 것, 이 API에 최대 파일 크기 2GB 제한이 있다는 것, 이 .NET API가 PKWARE사의 공식 ZIP 파일 형식 사양을 따르는 파일을 다룬다는 것에 대해. ↩
-
Microsoft Learn, Compress-Archive. Compress-Archive가 System.IO.Compression.ZipArchive API를 사용해 압축한다는 것, “The API limits the maximum file size to 2GB”라고 하여 2GB 상한이 기반 API 쪽의 제한이라고 명기되어 있다는 것, 참조처로 ZipArchive 클래스가 제시되어 있다는 것에 대해(ZipArchive 클래스의 레퍼런스 본문에는 2GB라는 숫자가 기재되어 있지 않기 때문에, 숫자의 1차 정보는 이 코맨드릿 쪽 페이지가 됩니다). ↩
-
Microsoft Learn, ZipFile Class. ZipFile 클래스가 ZIP의 생성・압축 해제・열기의 정적 메서드를 제공한다는 것, .NET Framework에서 사용하려면 System.IO.Compression.FileSystem 어셈블리 참조가 필요하다는 것, CreateFromDirectory/ExtractToDirectory/OpenRead의 용도에 대해. ↩ ↩2
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
PowerShell 스크립트의 인수 설계와 모듈화 ── 「작동하는 스크립트」에서 「남에게 넘길 수 있는 스크립트」로
PowerShell 스크립트를 다른 사람에게 넘길 수 있는 품질로 끌어올리는 절차를 정리합니다. param 블록과 [CmdletBinding()], 입력 검증, 파이프라인 입력, -WhatIf 대응, .psm1 모듈화, 사내 공유와 Git 관리의...
PowerShell로 Excel・CSV 업무 처리를 자동화한다 ── 집계・대조・장표 출력의 실무 레시피
PowerShell로 CSV 집계・대조와 Excel 장표 출력을 자동화하는 실무 레시피입니다. Import-Csv/Export-Csv의 문자 코드 기본값(5.1과 7의 차이), Group-Object 집계, Compare-Object와 해시테이블...
Windows PowerShell 5.1과 PowerShell 7의 차이 ── 사내 스크립트 이행 실무 가이드
Windows PowerShell 5.1과 PowerShell 7의 관계(공존과 pwsh.exe), 5.1은 신기능을 추가하지 않는다는 공식 방침, 인코딩 차이로 인한 문자 깨짐, #Requires를 통한 방어, 작업 스케줄러 갱신까지 이행 절차...
C#(CSharp)에서 PowerShell을 실행하고 결과를 객체로 받는 방법
C#에서 PowerShell을 실행하고 결과를 문자열이 아닌 PSObject로 받는 방법을, PowerShell SDK, AddCommand, AddParameter, BaseObject, Properties, 오류 처리까지 실무 관점에서 정리합니다.
PowerShell 실용 명령어 모음 ── 일상 업무에서 자주 쓰는 작은 기능 늘리기
PowerShell로 일상 업무에 쓰는 실용 명령어로서, Measure-Object, Group-Object, Select-String, Compare-Object, Tee-Object, Start-Transcript 등의 활용 시점을 정리합니다.
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
ActiveX 이관
COM / ActiveX / OCX 자산을 유지할지, 감쌀지, 교체할지의 단계적 판단을 정리한 토픽 페이지입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
기존 자산 활용 & 이관 지원
COM / ActiveX / OCX 자산, 네이티브 코드, 32비트 의존성을 유지하면서 단계적인 이관 계획을 지원합니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- PowerShell에서 .NET 클래스를 직접 호출할 수 있나요?
- 호출할 수 있습니다. PowerShell은 .NET 위에 구축되어 있기 때문에 [System.IO.Path]::GetFileNameWithoutExtension() 처럼 대괄호로 타입 이름을 쓰고 :: 로 정적 메서드를 호출할 수 있습니다. 인스턴스가 필요하면 New-Object나, PowerShell 5.0 이후에 사용할 수 있는 [타입]::new()로 생성합니다. 코맨드릿이 준비되어 있지 않은 처리라도 .NET 클래스 라이브러리에 있다면 추가 설치 없이 스크립트에서 사용할 수 있습니다.
- New-Object와 [타입]::new() 중 어느 쪽을 써야 하나요?
- 둘 다 객체는 만들 수 있지만, [타입]::new()는 인수 없이 입력하면 해당 타입의 생성자 목록이 표시되기 때문에 인수를 조사하면서 작성하는 데 적합합니다. 성능도 [타입]::new() 쪽이 유리합니다. 한편 COM 객체 생성은 New-Object -ComObject로만 할 수 있습니다. PowerShell 5.0보다 오래된 환경을 고려해야 한다면 New-Object를 사용합니다.
- PowerShell에서 Excel을 COM으로 조작하면 왜 EXCEL.EXE가 남나요?
- COM 객체는 참조 카운트로 수명이 관리되며, .NET 쪽에서는 RCW(Runtime Callable Wrapper)가 그 참조를 쥐고 있습니다. $excel.Workbooks.Open() 처럼 쓰면 중간의 Workbooks 객체에도 보이지 않는 RCW가 만들어져, 변수에 담지 않은 참조가 남습니다. Quit()을 호출해도 참조가 남아 있으면 프로세스는 종료되지 않습니다. 다 쓰고 나면 Marshal.ReleaseComObject로 명시적으로 해제하거나, 변수를 null로 만들고 GC.Collect()와 WaitForPendingFinalizers()로 회수시키는 뒷정리가 필요합니다.
- 서버나 야간 배치에서 Excel을 COM으로 조작해도 되나요?
- 권장되지 않습니다. Microsoft는 무인・비대화형 클라이언트 애플리케이션이나 구성 요소에서의 Office 애플리케이션 자동화를 권장하지 않으며 지원도 하지 않는다고 공식적으로 명시하고 있습니다. Office는 대화형 데스크톱 사용을 전제로 설계되어 있어, 무인 실행에서는 불안정한 동작이나 데드락이 발생할 수 있기 때문입니다. 무인 처리에서는 Open XML 계열 라이브러리나 CSV 연계 등, Office 소프트웨어 자체를 실행하지 않는 방식으로 대체하는 것이 정석입니다.
- Add-Type으로 Win32 API를 호출할 수 있나요?
- 호출할 수 있습니다. Add-Type의 -MemberDefinition에 DllImport가 붙은 C# 시그니처를 전달하면 그 자리에서 컴파일되어 P/Invoke를 통해 Win32 API를 호출할 수 있게 됩니다. 공식 문서에도 user32.dll의 ShowWindowAsync를 호출하는 예시가 실려 있습니다. 다만 추가한 타입은 세션 내에 남으며 같은 이름의 타입은 다시 정의할 수 없으므로, 시행착오는 새 세션에서 다시 실행하세요. 시그니처를 잘못 쓰면 프로세스째 다운되는 경우도 있으므로, 동작 확인은 일회용 콘솔에서 하는 것이 안전합니다.
- Windows PowerShell 5.1과 PowerShell 7은 .NET 호출에서 차이가 있나요?
- 있습니다. 5.1은 .NET Framework 4.5계열, PowerShell 7은 .NET(구 .NET Core) 위에 구축되어 있어 사용할 수 있는 타입이나 메서드 오버로드가 다릅니다. 예를 들어 String.Split의 오버로드는 7 쪽이 더 많아서, 같은 코드라도 결과가 달라지는 예시가 공식적으로 소개되어 있습니다. 또한 5.1에서는 GAC의 어셈블리를 Add-Type으로 로드해야 하는 경우가 많은 반면, 7에서는 동봉 어셈블리가 필요 시 자동으로 로드됩니다. 양쪽에서 동작시킬 스크립트는 두 환경 모두에서 테스트하세요.