수정 이력(5건, 최종 수정 2026년 09월 03일)
이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.
- Codex 리뷰에 따라 상담·문의 링크에 /ko/ 로케일 접두를 붙였습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- 글 맨 앞에 「이 글의 지식 맵」 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 정리한 것입니다. 본문의 주장은 바꾸지 않았습니다.
- 외부 리뷰(1283건)에 대한 대응으로 본문을 갱신했습니다. 개별 변경 내용은 아래 이력을 참조하십시오.
- 분류의 축을 한 줄로 정리했습니다(공식의 3개 범주는 멈추는 범위의 축이고, 이 글은 `try`/`catch`로 들어가는지의 2분류를 주축으로 쓴다는 관계입니다). 「멱등」의 첫 등장에 설명을 더하고, 스테이트먼트 종료 에러의 구체 예와 최소 코드를 추가했으며, `$PSNativeCommandUseErrorActionPreference`의 실험적 기능 이름과 5.1에서는 변수 자체가 존재하지 않는다는 점을 본문으로 올렸습니다.
- 에러 분류 설명이 정확하지 않았습니다. 먼저 비종료 에러와 종료 에러의 두 종류로 나뉘고, 종료 에러가 다시 나뉜다는 관계로 고쳤습니다.
- 최초 공개
이 글을 인용하기(DOI(등록된 아카이브): 10.5281/zenodo.22174596)
아래 DOI는 이전에 등록된 아카이브를 가리키며 현재 본문과 다를 수 있습니다. 현재 본문을 참조할 때는 이 페이지의 URL을 사용하세요.
Go Komura (2026). 「PowerShell의 에러 처리와 재실행 설계 ── try/catch가 동작하지 않는 함정부터 exit code·재시도의 정석까지」. 합동회사 코무라소프트. https://comcomponent.com/ko/blog/powershell-error-handling-retry-design/
- DOI(등록된 아카이브)
- 10.5281/zenodo.22174596
- DOI(마지막 등록 버전)
- 10.5281/zenodo.22174597
「야간 배치가 실패했는데도 작업 스케줄러에는 성공(0x0)으로 남아 아무도 몰랐다」「try/catch를 넣었는데 catch로 들어가지 않는다」「네트워크가 잠깐 끊겨 한 달에 한 번만 죽는다」── PowerShell 스크립트를 실제 운영에 넣으면, 이런 상담이 반드시 따라옵니다. 손으로 돌려 보고 만족하는 스크립트와, 무인으로 매일 밤 돌아가는 스크립트 사이에는 에러 처리와 재실행(재시도) 설계라는 벽이 있습니다.
까다로운 점은 PowerShell의 에러 모델이 일반적인 프로그래밍 언어의 예외 모델과 미묘하게 다르다는 점입니다. 「에러가 나는데도 처리가 계속된다」「catch한 줄 알았는데 빠져나간다」는 버그가 아니라 PowerShell 사양대로 움직이는 경우가 대부분입니다. 구조를 모른 채 작성하면 「실패를 삼킨 채 정상 종료하는 스크립트」가 양산됩니다.
이 글에서는 사내 정형 처리를 PowerShell로 자동화하는 정보시스템 담당자와 개발자를 대상으로, 종료 에러와 비종료 에러의 구별부터 네이티브 명령의 성패 판정, 작업 스케줄러나 모니터링에서 성패를 판정할 수 있는 exit code 설계, 일시적 에러를 버티는 재시도 패턴까지를 공식 문서의 근거와 함께 정리합니다.
1. 먼저 결론
- PowerShell의 에러는 「비종료 에러」와 「종료 에러(문 종료/스크립트 종료)」로 나뉩니다. 비종료 에러는 메시지를 표시하고 파이프라인을 계속 진행하며, 기본값에서는 try/catch로 들어가지 않습니다.1
- 세는 방식이 두 가지이므로 먼저 정리합니다. 공식 문서의 분류는 「비종료/문 종료/스크립트 종료」의 3개 범주이며, 이는 엔진이 「어디까지 멈출지」(파이프라인만/그 문만/호출 스택 전체)를 가리는 축입니다.1 한편 작성자가 맨 먼저 알고 싶은 것은 「try/catch로 들어가는가」이며, 이 축에서는 비종료 에러와 종료 에러의 두 종류가 됩니다. 이 글은 두 종류의 축을 주로 쓰고, 필요한 곳에서 3개 범주의 내역을 다룹니다.
- try/catch로 잡고 싶은 명령에는
-ErrorAction Stop을 붙이는 것이 정석입니다. Stop은 비종료 에러를 종료 에러로 승격시켜 catch에서 다루게 합니다.$ErrorActionPreference(기본값은 Continue)를 스크립트 맨 앞에서 Stop으로 두는 방법도 있습니다.12 -ErrorAction은 그 명령 하나에 대해$ErrorActionPreference를 덮어씁니다. 다만 둘은 완전히 대칭이 아니며,-ErrorAction이 제어할 수 있는 것은 비종료 에러뿐입니다.1- 네이티브 명령(robocopy, git, 외부 EXE)의 실패는 기본값에서 PowerShell 에러가 되지 않습니다. 0이 아닌 종료 코드는
$?를$false로 만들고$LASTEXITCODE에 들어가지만, ErrorRecord는 만들어지지 않고 catch로도 들어가지 않습니다. 성패는$LASTEXITCODE로 판정합니다.1 - PowerShell 7.4에서는
$PSNativeCommandUseErrorActionPreference가 정식 기능이 되었습니다.$true로 두면 0이 아닌 종료 코드가 비종료 에러를 일으키고,$ErrorActionPreference = 'Stop'과 함께 쓰면 try/catch로 잡을 수 있습니다(기본값은$false).32 - catch 안에서는
$_가 ErrorRecord입니다.$_.Exception으로 예외 본체에, 승격된 에러라면$_.Exception.ErrorRecord로 원래 에러 정보를 추적할 수 있습니다. 예외 타입을 지정한 catch 블록으로, 예상된 에러만 따로 처리할 수 있습니다.14 - 성패는 반드시 exit code로 외부에 알립니다.
exit키워드로 종료 코드를 설정하고,pwsh -File/powershell.exe -File로 시작하면 그 값이 프로세스의 종료 코드가 됩니다. exit가 없으면 정상 종료 0·처리되지 않은 예외 1입니다.56 - 재시도는 일시적 에러 한정·상한 있음·멱등(같은 처리를 몇 번 실행해도 결과가 바뀌지 않는 성질. 6장에서 자세히 다룹니다)이 세 원칙입니다. 업무 에러를 재시도로 얼버무리지 말 것, 지수 백오프로 간격을 넓힐 것, 다시 실행해도 이중 처리가 되지 않게 설계할 것. 이 세 가지가 갖춰져야 비로소 「다시 실행해도 되는 스크립트」가 됩니다.
그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 18건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle
2. 두 종류의 에러 ── 왜 try/catch가 동작하지 않는가
PowerShell의 에러는 먼저 비종료 에러와 종료 에러의 두 종류로 나뉩니다. 그리고 종료 에러는 문 종료 에러와 스크립트 종료 에러로 나뉘므로, 잘게 세면 3개 범주가 됩니다. 비종료 에러는 파이프라인을 멈추지 않고 보고만 하고, 문 종료 에러는 그 문만 멈춘 뒤 다음 문으로 나아가며, 스크립트 종료 에러는 호출 스택 전체를 되돌립니다.1
현장에서 함정이 되는 것은 비종료 에러입니다. Get-Content나 Get-ChildItem 같은 cmdlet이 개별 입력 처리에 실패했을 때 내는 것은 대개 비종료 에러이며, 빨간 에러 메시지는 나오지만 처리는 계속되고, try/catch에도 trap에도 들어가지 않습니다.1
# 【함정】catch로는 한 번도 들어가지 않고, "완료"까지 표시된다
try {
Get-Content -Path 'C:\Data\存在しないファイル.txt' # 비종료 에러
Write-Host '완료' # 실행되어 버린다
}
catch {
Write-Host '여기로는 오지 않는다'
}
# 【정석】-ErrorAction Stop으로 종료 에러로 승격시켜, catch에서 다루게 한다
try {
Get-Content -Path 'C:\Data\存在しないファイル.txt' -ErrorAction Stop
Write-Host '완료' # 에러 시에는 실행되지 않는다
}
catch {
Write-Host "잡았다: $($_.Exception.Message)"
}
-ErrorAction Stop이나 $ErrorActionPreference = 'Stop'이 걸려 있으면, 엔진은 비종료 에러를 ActionPreferenceStopException으로 감싸 종료 에러로 승격시킵니다. try 블록 안에서는 이 승격된 에러가 catch로 도착한다는 것이 정확한 메커니즘입니다.1
한편, 처음부터 종료 에러가 되는(=아무것도 하지 않아도 catch로 들어가는) 것도 있습니다. 문 종료 에러가 그것이며, 공식 문서는 다음 발생원을 들고 있습니다.1
- 없는 명령을 호출했을 때(
CommandNotFoundException) - 매개변수 바인딩에 실패했을 때(
ParameterBindingException. 숫자 매개변수에 변환할 수 없는 문자열을 넘긴 경우 등) - .NET 메서드가 예외를 던졌을 때(
[int]::Parse('abc')등) - cmdlet이나 고급 함수가
$PSCmdlet.ThrowTerminatingError()로 「이 호출은 더 이상 이어갈 수 없다」고 보고했을 때
이름 그대로 「그 문만」 멈추므로, 스크립트는 다음 문부터 실행이 이어집니다.1
# 문 종료 에러: 이 문은 멈추지만, 다음 문은 실행된다
[int]::Parse('abc')
Write-Output '이 줄은 실행된다'
# 종료 에러이므로, -ErrorAction Stop을 붙이지 않아도 catch로 들어간다
try { [int]::Parse('abc') }
catch { Write-Warning "잡았다: $($_.Exception.Message)" }
까다로운 점은, 「파일이 없다」 같은 같은 상황이라도 cmdlet 쪽 구현에 따라 비종료 에러이기도 하고 문 종료 에러이기도 하다는 점입니다. 작성하는 쪽에서 매번 가리는 것은 비현실적이므로, 잡고 싶은 줄에는 -ErrorAction Stop을 명시해 어느 쪽이든 반드시 catch로 도착하는 형태로 맞춰 버리는 것이 실무의 답입니다.
「그렇다면 항상 $ErrorActionPreference = 'Stop'으로 두면 되지 않나」라는 발상은 절반은 맞습니다. 무인 실행 스크립트에서는 에러를 삼키고 진행하는 것보다, 멈춰서 실패를 보고하는 편이 안전하므로, 맨 앞에서 Stop으로 두는 것은 좋은 기본값입니다. 다만 $ErrorActionPreference는 그 스코프와 자식 스코프에 영향을 주므로, 호출한 모듈이나 함수의 동작까지 바뀐다는 점, 실패해도 되는 뒷정리(임시 파일 삭제 등)에는 개별적으로 -ErrorAction SilentlyContinue를 다시 붙여야 한다는 점은 의식해야 합니다.2
3. catch 안에서 무엇을 읽을 것인가 ── ErrorRecord 따라가기
catch 블록의 $_에는 ErrorRecord가 들어 있습니다. 로그에 남길 정보는 여기서 가져옵니다.14
try {
Copy-Item -Path $src -Destination $dest -ErrorAction Stop
}
catch [System.IO.IOException] {
# 타입을 지정한 catch로 「예상된 실패」만 따로 처리한다.
# 승격된 에러라도, 엔진이 원래 예외 타입으로 맞춰 준다
Write-Warning "I/O 에러: $($_.Exception.Message)"
}
catch {
# 예상하지 못한 실패는 컨텍스트까지 로그에 남기고 다시 던진다(삼키지 않는다)
$rec = $_ # $_는 ErrorRecord
Write-Warning ('종류: {0} / 위치: {1} / 대상: {2}' -f `
$rec.Exception.GetType().FullName,
$rec.InvocationInfo.PositionMessage,
$rec.TargetObject)
throw # 인수 없는 throw로 같은 에러를 상위로 전파
}
finally {
# finally는 성공 시에도 에러 시에도, Ctrl+C로 멈춰도 실행된다. 뒷정리는 여기로
if ($tempFile -and (Test-Path $tempFile)) { Remove-Item $tempFile -ErrorAction SilentlyContinue }
}
읽을 지점은 세 가지입니다.
$_.Exception이 예외 본체입니다.-ErrorAction Stop으로 승격된 에러는ActionPreferenceStopException으로 감싸지지만, catch의 타입 매칭에서는 엔진이 원래 예외 타입(예:ItemNotFoundException)을 봐 주므로 타입 지정 catch는 그대로 작성하면 됩니다. 원래 ErrorRecord에는$_.Exception.ErrorRecord로 추적할 수 있습니다.1$_.InvocationInfo.PositionMessage에 「어느 파일의 몇 번째 줄의 어느 명령인지」가 들어 있으며, 무인 실행 로그에서는 이것이 있느냐 없느냐로 조사 시간이 자릿수 단위로 달라집니다.- finally 블록은 try가 성공해도, 에러가 나도, Ctrl+C로 멈춰도 실행됩니다. 연결을 닫거나 임시 파일을 지우는 뒷정리는 finally에 둡니다.7
어느 층에서 catch하고 어디에서 로그를 쓸 것인가라는 설계론은 언어를 넘어 공통입니다. 「예외 처리에서 catch와 로그는 어디에 두어야 하는가」에서 정리한 원칙(경계에서 catch한다, 삼키지 않는다, 이중 로그를 피한다)은 PowerShell에도 그대로 적용할 수 있습니다.
4. 네이티브 명령의 성패 ── $?와 $LASTEXITCODE, 그리고 7.4의 새 기능
또 하나의 큰 구멍이 robocopy나 git, 사내 EXE 같은 네이티브 명령입니다. 외부 프로그램은 PowerShell의 에러 시스템에 참여하지 않고, 실패를 종료 코드로 보고합니다. 기본 동작은 다음과 같습니다.1
| 사건 | 동작(기본값) |
|---|---|
| 0이 아닌 종료 코드 | $?가 $false가 되고, $LASTEXITCODE에 종료 코드가 들어간다 |
| ErrorRecord 생성 | 되지 않는다($Error에도 추가되지 않는다) |
| try/catch | 들어가지 않는다 |
즉, try { robocopy ... } catch { ... }는(기본값에서는) 아무것도 잡지 못합니다. 네이티브 명령의 성패 판정은 $LASTEXITCODE로 씁니다. $?는 「직전 작업이 성공했는가」의 불리언 값이며, 네이티브 명령에 대해서는 종료 코드가 0일 때만 $true가 됩니다.1 참고로 Windows PowerShell 5.1에서는 네이티브 명령이 stderr에 쓰기만 해도 $?가 $false가 되는 경우가 있었지만, PowerShell 7에서는 0이 아닌 종료 코드일 때만 $false가 되도록 바뀌었습니다. stderr 출력을 실패로 보지 않는다는, 실제에 맞춘 변경입니다.8
# 네이티브 명령은 $LASTEXITCODE로 판정한다
robocopy.exe 'D:\Reports' '\\fileserver\reports' /MIR /R:2 /W:5
if ($LASTEXITCODE -ge 8) {
# robocopy는 0〜7이 성공 계열(복사 유무 등의 정보), 8 이상이 실패
throw "robocopy가 실패했습니다 (ExitCode=$LASTEXITCODE)"
}
PowerShell 7.4부터는 이 취급을 바꾸는 $PSNativeCommandUseErrorActionPreference를 쓸 수 있습니다. 7.3에서 실험적 기능으로 추가되었고, 7.4에서 정식 기능(mainstream)이 되었습니다.3 $true로 두면, 0이 아닌 종료 코드의 네이티브 명령이 종료 코드를 명시한 비종료 에러를 일으키고, 이것이 $ErrorActionPreference를 따릅니다. 즉 Stop과 함께 쓰면 외부 명령의 실패도 try/catch로 잡을 수 있습니다.12
5.1과 7이 섞인 환경에서는 버전 전제를 반드시 확인해야 합니다. 이 기능을 쓸 수 있는 것은 PowerShell 7.4 이후이며, 7.3에서는 실험적 기능(기능 이름은 PSNativeCommandErrorActionPreference)이었기 때문에 Enable-ExperimentalFeature로 켠 뒤 세션을 다시 시작해야 했습니다.3 그리고 Windows PowerShell 5.1에는 이 변수 자체가 없어서, $true를 넣어도 아무 일도 일어나지 않습니다(새 변수만 만들어질 뿐이며, 에러도 나지 않아 알아채기 어렵습니다). 같은 스크립트를 5.1과 7 양쪽에서 돌릴 가능성이 있다면, 이 기능에 기대지 말고 다음 장 이후를 포함해 $LASTEXITCODE 판정으로 통일하는 편이 안전합니다.
# PowerShell 7.4 이후: 외부 명령의 실패도 try/catch로 다룬다(기본값은 $false)
$PSNativeCommandUseErrorActionPreference = $true
$ErrorActionPreference = 'Stop'
try {
git.exe fetch origin
}
catch {
Write-Warning "git이 실패: $($_.Exception.Message)"
throw
}
& {
# robocopy처럼 0이 아님=실패가 아닌 명령은, 스크립트 블록 안에서
# 잠시 끄고 기존대로 $LASTEXITCODE로 판정한다(빠져나오면 원래대로 돌아간다)
$PSNativeCommandUseErrorActionPreference = $false
robocopy.exe 'D:\Reports' '\\fileserver\reports' /MIR
if ($LASTEXITCODE -ge 8) { throw "robocopy 실패 (ExitCode=$LASTEXITCODE)" }
}
robocopy 예는 공식 문서에도 그대로 실려 있듯이, 0이 아닌 종료 코드를 정상적인 정보로 쓰는 명령이 존재하기 때문에, 한꺼번에 켜려면 예외 구간 설계가 필요합니다.2 Windows PowerShell 5.1만 쓸 수 있는 현장에서는 이 기능이 없으므로 $LASTEXITCODE 판정으로 통일해야 합니다. 5.1과 7의 동작 차이는 이전 시의 함정이 되기 쉬우므로, 「Windows PowerShell 5.1과 PowerShell 7의 차이와 이전」도 함께 참고해야 합니다.
5. exit code 설계 ── 작업 스케줄러와 모니터링에서 성패를 판정할 수 있게 한다
에러를 잡았다면, 다음은 외부로의 보고입니다. 작업 스케줄러나 모니터링 도구가 스크립트의 성패를 아는 수단은, 사실상 프로세스의 종료 코드뿐입니다. 사양을 정확히 짚습니다.
exit <숫자>로 스크립트의 종료 코드를 명시할 수 있습니다.exit는$LASTEXITCODE에도 값을 넣습니다.59pwsh -File(powershell.exe -File)로 시작한 경우, exit로 지정한 값이 그대로 프로세스의 종료 코드가 됩니다. exit 문이 없으면, 정상 완료는 0, 처리되지 않은 예외로 끝나면 1입니다.56-Command로 스크립트를 시작하면, 스크립트 안의exit 10같은 종료 코드는 유지되지 않습니다. 마지막 명령의 성패로부터 0 또는 1로 뭉개집니다(명령 문자열에 직접exit 10이라고 쓴 경우에는 그 값이 돌아옵니다). 스크립트의 종료 코드를 나눠 쓰는 운영이라면-File시작이 정석입니다.6
이 사양을 뼈대로 옮기면, 무인 실행 스크립트의 템플릿은 다음과 같습니다.
# Invoke-NightlyExport.ps1 ── 작업 스케줄러에서 성패를 판정할 수 있는 뼈대
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop' # 무인 실행에서는 「멈춰서 보고」를 기본값으로 한다
# 표준 출력·에러를 포함한 실행 기록을 로그로 남긴다(-Append로 일별 파일에 이어 쓴다)
Start-Transcript -Path "C:\Logs\NightlyExport_$(Get-Date -Format yyyyMMdd).log" -Append
try {
Export-DailyData # 업무 처리 본체(모듈화한 함수를 호출)
exit 0 # 성공을 명시
}
catch [System.Net.WebException] {
Write-Warning "통신 에러: $($_.Exception.Message)"
exit 10 # 일시적 에러 계열 ── 작업 쪽에서 재실행을 구성할 여지를 남긴다
}
catch {
Write-Warning "예상하지 못한 에러: $($_.Exception.Message)"
Write-Warning $_.InvocationInfo.PositionMessage
exit 1 # 영구적 에러 ── 재실행하지 않고 사람이 본다
}
finally {
Stop-Transcript # finally라면 exit를 거쳐도 기록이 닫힌다
}
Start-Transcript는 세션의 입력과 출력을 통째로 텍스트에 기록하는 cmdlet이며, echo나 리다이렉트를 심지 않아도 「그때 화면에 무엇이 나와 있었는지」를 재현할 수 있습니다.10 자체 로그 함수와 배타적이지 않으며, 마지막 보루로 함께 쓸 가치가 있습니다. 로그 설계와 비대해짐 대책은 「PowerShell 스크립트 응용 ── 로그 조사·아카이브·리포트화를 안전하게 자동화한다」에서 다루고 있습니다.
exit code 배분은 너무 공들이지 않는 것이 요령입니다. 0=성공, 1=영구적 에러(사람이 본다), 10번대=일시적 에러(다시 실행해도 된다) 정도의 굵기로 충분하며, 작업 스케줄러의 「이전 실행 결과」나 잡 관리 도구의 성패 판정을 그대로 짤 수 있습니다. 작업 쪽 설정(실패 시 재실행, 실행 결과 확인 방법)은 「작업 스케줄러의 작업이 실행되지 않는다·0x1로 끝난다 ── 원인 가름과 안전한 운영 설계」를 참고해야 합니다.
6. 재시도 설계 ── 일시적 에러와 업무 에러를 가른다
마지막으로 재실행입니다. 재시도의 가치는 「일시적 에러를 자동으로 흡수해, 한밤중에 사람을 깨우지 않는 것」이지만, 대충 넣으면 「영구적 실패를 한없이 다시 시도한다」「이중 처리로 데이터를 망가뜨린다」는 다른 사고를 만듭니다. 원칙은 세 가지입니다.
- 재시도하는 것은 일시적 에러뿐입니다. 네트워크의 순간 끊김, 파일의 일시 잠금, 의존 서비스의 시작 대기처럼 시간이 해결할 수 있는 실패에 한정합니다. 입력 오류·권한 부족·설정 실수는 즉시 실패시키고, exit code와 로그로 사람에게 넘깁니다.
- 상한과 간격을 설계합니다. 횟수 상한을 정하고, 간격은 지수 백오프(2초, 4초, 8초…)로 넓힙니다. 장애 중인 상대를 일정한 간격으로 계속 두드리는 것은 회복을 방해할 뿐입니다.
- 멱등(다시 실행해도 안전)하게 해 둡니다. 재시도도 작업 스케줄러의 재실행도, 「같은 처리가 한 번 더 돈다」는 뜻입니다. 출력은 임시 파일에 쓴 뒤 이름을 바꿔 공개하고, 처리한 ID를 기록해 이중 반입을 걸러 내는 식의 설계가 전제입니다.
패턴으로는 이 형태로 모을 수 있습니다.
function Invoke-WithRetry {
[CmdletBinding()]
param(
[Parameter(Mandatory)] [scriptblock] $Operation,
# 0 이하를 넘기면 한 번도 실행하지 않고 정상 종료하므로, 1 이상을 강제한다
[ValidateRange(1, 100)]
[int] $MaxAttempts = 4,
# 음수는 재시도 시 Start-Sleep에서 다른 에러가 되므로, 바인딩 시점에 걸러 낸다
[ValidateRange(0, 3600)]
[int] $BaseDelaySeconds = 2,
# 다시 시도할 가치가 있는 예외 타입만 열거한다(기본값은 통신·I/O 계열)
[Type[]] $RetryableExceptions = @([System.IO.IOException], [System.Net.WebException])
)
for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
try {
# 출력은 일단 변수로 받고, 성공한 뒤에 반환한다. & $Operation을 바로 return하면,
# 도중까지 출력한 뒤 예외가 난 경우, 부분 출력이 호출 쪽으로 흘러가
# 재시도가 성공했을 때 같은 데이터가 이중으로 도착한다
$output = & $Operation
return $output
}
catch {
$ex = $_.Exception
$isRetryable = $RetryableExceptions | Where-Object { $ex -is $_ }
if (-not $isRetryable -or $attempt -eq $MaxAttempts) {
throw # 업무 에러, 또는 재시도 상한 ── 그대로 실패시킨다
}
# 지수로 늘어나는 대기 시간에 상한을 둔다(횟수가 많은 구성에서도 너무 기다리지 않고,
# Start-Sleep이 받는 범위도 넘지 않는다)
$delay = [math]::Min($BaseDelaySeconds * [math]::Pow(2, $attempt - 1), 300)
Write-Warning "실패(${attempt}회째): $($ex.Message) ── ${delay}초 후에 다시 시도합니다"
Start-Sleep -Seconds $delay
}
}
}
# 사용법: 대상 처리는 -ErrorAction Stop으로 종료 에러화해 둘 것
Invoke-WithRetry -Operation {
Copy-Item -Path '\\fileserver\out\daily.csv' -Destination 'D:\Work' -ErrorAction Stop
}
# PowerShell 7의 Invoke-RestMethod / Invoke-WebRequest를 다시 시도할 때의 주의:
# 7에서는 통신 실패가 5.1 시절의 WebException이 아니라 HttpRequestException 계열로 오므로,
# 기본값 그대로는 재시도되지 않는다. 또한 404 같은 영구적 HTTP 에러 응답도 같은 타입으로
# 오므로, 응답이 돌아온 경우에는 상태 코드로 「다시 시도할 가치」를 직접 가른다
Invoke-WithRetry -RetryableExceptions ([System.Net.Http.HttpRequestException]) -Operation {
# -SkipHttpErrorCheck로 에러 응답이어도 예외로 만들지 않고 받아, 코드를 보고 나눠 던진다
$r = Invoke-WebRequest -Uri 'https://api.example.co.jp/orders' -TimeoutSec 30 -SkipHttpErrorCheck
if ($r.StatusCode -in 408, 429, 500, 502, 503, 504) {
# 일시적 코드만 HttpRequestException으로 던진다 → 재시도된다
throw [System.Net.Http.HttpRequestException]::new("일시적 HTTP 에러: $($r.StatusCode)")
}
if ($r.StatusCode -ge 400) {
throw "영구적 HTTP 에러: $($r.StatusCode)" # 타입이 다르므로 재시도되지 않는다
}
$r.Content | ConvertFrom-Json
}
핵심은 재시도 대상을 예외 타입으로 명시적으로 고르고 있다는 점입니다. 「catch하면 전부 재시도」라고 쓰면, 매개변수 실수 같은 영구적 에러까지 4번 시도하며 헛되이 기다리게 됩니다. 운영을 시작한 뒤에는, 실제 로그에서 관측한 일시적 에러 타입을 $RetryableExceptions에 보태 가는 방식이 현실적입니다.
참고로 Invoke-WithRetry 본체는 긴 편이므로, 쓸 때마다 스크립트에 붙여 넣지 말고, Retry.psm1 같은 파일에 통째로 저장해 Import-Module로 읽는 형태로 해야 합니다. 복사할 때마다 catch 안만 옛 버전이 섞이는 사고를 막을 수 있습니다(모듈화 방법은 「PowerShell의 인수 설계와 모듈화」 참고). 또한 재시도와 에러 분기 로직은 바로 Pester로 테스트를 쓸 가치가 있는 부분입니다(「Pester로 하는 PowerShell 테스트 정비 ── 운영 스크립트를 쉽게 깨지지 않게 하는 실무의 형」).
7. 실무의 정석(판단표)
| 논점 | 선택지 | 판단 기준 |
|---|---|---|
| 에러의 기본 동작 | Continue 그대로 / 맨 앞에서 $ErrorActionPreference = ‘Stop’ | 무인 실행은 「멈춰서 보고」가 안전. 대화형 조사용 스크립트는 Continue 그대로여도 된다2 |
| catch하고 싶은 곳 | 바라기만 한다 / -ErrorAction Stop을 명시 | cmdlet은 비종료 에러가 많다. 잡고 싶은 줄에는 Stop을 명시한다1 |
| 네이티브 명령의 성패 | 방치 / $LASTEXITCODE 판정 / 7.4의 $PSNativeCommandUseErrorActionPreference | 5.1이 섞인 환경은 $LASTEXITCODE 판정으로 통일. 7.4 이후만이면 새 기능+robocopy 등의 예외 구간32 |
| 성패의 외부 보고 | 로그만 / exit code를 설계해 -File로 시작 | 로그는 사람용, exit code는 기계용. 둘 다 필요. -Command 시작은 종료 코드가 뭉개진다6 |
| 실행 기록 | 자체 로그만 / Start-Transcript 병용 | 자체 로그가 못 잡는 출력(외부 명령의 표준 출력 등)까지 남길 수 있는 보험10 |
| 재시도 | 모든 에러 대상 / 일시적 에러 한정+지수 백오프+멱등 | 업무 에러의 재시도는 사고의 씨앗. 상한·간격·멱등 세 점 세트로 |
8. 정리
- PowerShell의 에러는 비종료 에러와 종료 에러로 나뉘며, 비종료 에러는 기본값에서 try/catch로 들어가지 않습니다. 잡고 싶은 명령에는
-ErrorAction Stop을 명시하는 것이 정석입니다. $ErrorActionPreference의 기본값은 Continue입니다. 무인 실행 스크립트는 맨 앞에서 Stop으로 두어, 「실패를 삼킨 채 정상 종료하는」 사고를 구조적으로 막습니다.- 네이티브 명령의 실패는 기본값에서 catch로 들어가지 않습니다.
$LASTEXITCODE로 판정하거나, PowerShell 7.4 이후라면$PSNativeCommandUseErrorActionPreference를 활용합니다. - catch에서는
$_(ErrorRecord)에서 예외의 타입·메시지·위치 정보를 로그에 남기고, 뒷정리는 finally에 둡니다. finally는 Ctrl+C나 exit에서도 실행됩니다. - 성패는 exit code로 외부에 보고합니다.
-File시작이라면exit의 값이 그대로 종료 코드가 되어, 작업 스케줄러나 모니터링에서 성패를 판정할 수 있습니다. - 재시도는 일시적 에러 한정·상한 있는 지수 백오프·멱등의 세 원칙으로. 영구적 에러는 즉시 실패시켜 사람에게 넘깁니다.
관련 글
- PowerShell 스크립트 응용 ── 로그 조사·아카이브·리포트화를 안전하게 자동화한다
- Pester로 하는 PowerShell 테스트 정비 ── 운영 스크립트를 쉽게 깨지지 않게 하는 실무의 형
- 작업 스케줄러의 작업이 실행되지 않는다·0x1로 끝난다 ── 원인 가름과 안전한 운영 설계
- 예외 처리에서 catch와 로그는 어디에 두어야 하는가
- PowerShell의 실행 정책과 스크립트 서명
- PowerShell의 인수 설계와 모듈화
관련 상담 영역
합동회사 코무라소프트에서는 야간 배치·정형 처리 스크립트의 에러 처리와 재시도 설계 리뷰, 「실패했는데 성공으로 처리된다」「한 달에 한 번만 죽는다」 같은 간헐 장애 조사, 기존 스크립트 자산의 운영 품질 개선을 다루고 있습니다.
참고 링크
-
Microsoft Learn, about_Error_Handling. 비종료 에러·문 종료 에러·스크립트 종료 에러의 3분류, 비종료 에러가 기본값에서 catch/trap로 들어가지 않는다는 점, -ErrorAction Stop에 의한 승격 구조(ActionPreferenceStopException과 $_.Exception.ErrorRecord), 타입 지정 catch가 원래 예외 타입으로 맞는다는 점, $?와 $LASTEXITCODE의 사양, 네이티브 명령의 0이 아닌 종료 코드가 기본값에서 ErrorRecord를 만들지 않는다는 점, $PSNativeCommandUseErrorActionPreference의 동작에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17
-
Microsoft Learn, about_Preference_Variables. $ErrorActionPreference의 기본값이 Continue라는 점, -ErrorAction 매개변수가 개별 명령에서 우선된다는 점, 설정이 스코프와 자식 스코프에 적용된다는 점, $PSNativeCommandUseErrorActionPreference의 기본값이 $false라는 점과, robocopy처럼 0이 아닌 종료 코드를 정보로 쓰는 명령에서 스크립트 블록 안에서 잠시 끄는 예에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, What’s New in PowerShell 7.4. 실험적 기능 PSNativeCommandErrorActionPreference($PSNativeCommandUseErrorActionPreference)가 PowerShell 7.4에서 정식 기능(mainstream)이 되었다는 점에 대해. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Everything you wanted to know about exceptions. catch 블록 안에서 $_에서 예외 정보에 접근할 수 있다는 점, -ErrorAction Stop을 붙인 명령과 Write-Error의 에러가 catch에서 다루어지게 된다는 점, try/finally에 의한 리소스 해제 패턴에 대해. ↩ ↩2
-
Microsoft Learn, about_Language_Keywords. exit 키워드가 종료 코드를 설정하고 $LASTEXITCODE에도 반영된다는 점, pwsh -File로 시작한 스크립트가 exit의 숫자 인수를 종료 코드로 반환한다는 점, exit 문이 없으면 정상 완료는 0·처리되지 않은 예외는 1이 된다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, about_Pwsh. -File 시작 시 종료 코드가 정해지는 방식, -Command 시작에서는 0과 1 이외의 종료 코드가 1로 바뀌므로 종료 코드를 유지하려면 exit $LASTEXITCODE가 필요하다는 점에 대해. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, about_Try_Catch_Finally. try/catch/finally 구문, 타입 지정 catch 블록과 여러 catch, finally 블록이 성공 시·에러 시에 더해 Ctrl+C로 멈출 때나 catch 안의 exit 시에도 실행된다는 점에 대해. ↩
-
Microsoft Learn, Differences between Windows PowerShell 5.1 and PowerShell 7.x. PowerShell 7에서는 네이티브 명령이 stderr에 쓰기만 해서는 $?가 $false가 되지 않고, 0이 아닌 종료 코드일 때만 $false가 되도록 바뀌었다는 점에 대해. ↩
-
Microsoft Learn, about_Automatic_Variables. $LASTEXITCODE가 네이티브 프로그램이나 스크립트의 종료 코드를 가진다는 점, pwsh -File 호출 시 예외 종료는 1·exit 키워드의 값·정상 완료는 0이 설정된다는 점에 대해. ↩
-
Microsoft Learn, Start-Transcript. 세션의 명령과 콘솔 출력을 텍스트 파일에 기록한다는 점, -Append로 이어 쓴다는 점, 기본 저장 위치와 파일 이름, Stop-Transcript로 멈춘다는 점에 대해. ↩ ↩2
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
PowerShell 스크립트가 느릴 때 볼 곳 ── 배열·파이프라인·매칭의 핵심
PowerShell 스크립트가 느려지는 대표적인 원인을 정리합니다. 배열의 +=가 O(n^2)가 되는 이유, 파이프라인과 foreach의 차이, 매칭의 해시 테이블화, 파일 I/O 개선, 올바른 측정 방법까지 실무 관점에서 설명합니다.
Write-Host를 그만두기 ── PowerShell의 출력 스트림과 로그 설계
PowerShell 6가지 출력 스트림의 구분 사용, Write-Host가 가진 문제와 올바른 쓰임새, 함수 반환값이 오염되는 원인, -Verbose와 -InformationVariable로 호출 측에서 제어하는 방법, 구조화 로그를 남기는 방법...
PowerShell의 병렬 처리 ── ForEach-Object -Parallel과 Job의 구분
ForEach-Object -Parallel·Start-ThreadJob·Start-Job의 차이와 구분, $using:와 스레드 안전성, ThrottleLimit 정하는 법, 오히려 느려지는 경우까지 실무 관점에서 정리합니다.
PowerShell에서 외부 exe를 올바르게 호출하기 ── 인자의 따옴표·종료 코드·문자 깨짐의 함정
PowerShell에서 robocopy나 사내 EXE를 호출하면 인자가 깨지고, 종료 코드를 얻지 못하며, 출력이 깨집니다. PowerShell 7.3의 인자 전달 변경, 구문 분석 중지 토큰 --%, Start-Process의 용도 나누기까지 ...
PowerShell에서 자격 정보를 안전하게 다루기 ── 스크립트에서 평문 비밀번호를 없애기
PowerShell 스크립트의 평문 비밀번호를 안전한 보관으로 옮기는 절차를 정리합니다. SecureString의 실체와 한계, Export-Clixml을 통한 DPAPI 저장 구조, SecretManagement/SecretStore를 쓰는 지...
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- PowerShell에서 try/catch를 넣었는데 catch로 들어가지 않는 이유는 무엇인가요?
- cmdlet이 내는 에러의 상당수가 비종료 에러(non-terminating error)이기 때문입니다. try/catch가 잡는 것은 종료 에러뿐이며, 비종료 에러는 메시지를 표시한 뒤 처리를 계속하므로 catch로는 들어가지 않습니다. 대처의 정석은 잡고 싶은 명령에 -ErrorAction Stop을 붙이는 것(또는 스크립트 맨 앞에서 $ErrorActionPreference = 'Stop'으로 두는 것)입니다. 이렇게 하면 비종료 에러가 종료 에러로 승격되어 try/catch에서 다룰 수 있게 됩니다.
- $?와 $LASTEXITCODE는 어떻게 나눠 쓰나요?
- $?는 직전 작업이 성공했는지를 나타내는 불리언 값이며, cmdlet과 네이티브 명령 모두에 설정됩니다. $LASTEXITCODE는 마지막으로 실행한 네이티브 프로그램(또는 exit한 스크립트)의 종료 코드이며, cmdlet 에러로는 바뀌지 않습니다. robocopy나 git 같은 외부 명령의 성패를 판정할 때는, 종료 코드의 의미까지 확인할 수 있는 $LASTEXITCODE로 보는 편이 확실합니다. 네이티브 명령의 0이 아닌 종료 코드는 기본값에서는 catch로 들어가지 않는다는 점에 주의해야 합니다.
- PowerShell 스크립트의 성패를 작업 스케줄러에서 판정하려면 어떻게 하나요?
- 스크립트의 마지막(과 catch 블록)에서 exit 키워드로 종료 코드를 명시하고, 작업 쪽은 pwsh -File(또는 powershell.exe -File)로 시작해 「이전 실행 결과」 값을 감시합니다. -File로 시작한 경우, exit로 지정한 값이 그대로 프로세스의 종료 코드가 되며, exit가 없으면 정상 종료는 0, 처리되지 않은 예외는 1이 됩니다. -Command로 시작하면 0과 1 이외의 종료 코드가 1로 바뀌므로, 종료 코드로 운영을 짜려면 -File 시작이 정석입니다.
- 재시도는 어떤 에러에 대해 해야 하나요?
- 다시 시도하면 결과가 바뀔 수 있는 일시적 에러(네트워크의 순간 끊김, 파일의 일시적 잠금, 서비스 시작 대기 등)에 한정합니다. 입력 데이터 오류나 권한 부족, 설정 실수 같은 업무 에러·영구적 에러는 몇 번을 해도 실패하므로 재시도하지 않고 즉시 실패시킨 뒤, 로그와 exit code로 사람에게 알립니다. 재시도하는 경우에도 횟수와 간격에 상한을 두고 지수 백오프로 간격을 넓힐 것, 그리고 다시 실행해도 이중 처리가 되지 않도록 처리를 멱등하게 설계해 두는 것이 전제입니다.
- PowerShell 7.4의 $PSNativeCommandUseErrorActionPreference는 무엇을 하는 설정인가요?
- 네이티브 명령이 0이 아닌 종료 코드로 끝났을 때 PowerShell 에러(비종료 에러)를 발생시키는 설정입니다. PowerShell 7.3에서 실험적 기능으로 추가되었고 7.4에서 정식 기능이 되었습니다(기본값은 $false). $true로 두면 $ErrorActionPreference를 따르므로, Stop과 함께 쓰면 외부 명령의 실패를 try/catch로 잡을 수 있습니다. 다만 robocopy처럼 0이 아닌 종료 코드를 정상적인 정보로 쓰는 명령도 있으므로, 그 구간만 $false로 되돌리는 식의 주의가 필요합니다.