PowerShell의 에러 처리와 재실행 설계 ── try/catch가 먹히지 않는 함정부터 exit code·재시도의 정석까지

· · PowerShell, Windows, 에러 처리, 재시도, 자동화, 운영 개선, 스크립트, 작업 스케줄러

「야간 배치가 실패했는데 작업 스케줄러상으로는 성공(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장에서 자세히 설명)이 세 가지 원칙입니다. 업무 에러를 재시도로 얼버무리지 않는 것, 지수 백오프로 간격을 넓히는 것, 재실행해도 이중 처리가 되지 않는 설계로 만드는 것. 이 세 가지가 갖춰져야 비로소 「재실행해도 되는 스크립트」가 됩니다.

2. 두 종류의 에러 ── 왜 try/catch가 먹히지 않는가

PowerShell의 에러는 먼저 비종료 에러종료 에러의 두 종류로 나뉩니다. 그리고 종료 에러는 구문 종료 에러스크립트 종료 에러로 나뉘므로, 세밀하게 세면 3개 카테고리가 됩니다. 비종료 에러는 파이프라인을 멈추지 않고 보고만 하고, 구문 종료 에러는 그 구문만 멈추고 다음 구문으로 진행하며, 스크립트 종료 에러는 호출 스택 전체를 되감습니다.1

현장에서 함정이 되는 것은 비종료 에러입니다. Get-ContentGet-ChildItem 같은 코맨드릿이 개별 입력의 처리에 실패했을 때 내는 것은 대개 비종료 에러로, 빨간 에러 메시지는 표시되지만 처리는 계속되고 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') 등)
  • 코맨드릿이나 고급 함수가 $PSCmdlet.ThrowTerminatingError()로 「이 호출은 더 이상 계속할 수 없다」고 보고했을 때

이름 그대로 「그 구문만」을 멈추므로, 스크립트는 다음 구문부터 실행이 계속됩니다.1

# 구문 종료 에러: 이 구문은 멈추지만, 다음 구문은 실행된다
[int]::Parse('abc')
Write-Output '이 줄은 실행된다'

# 종료 에러이므로, -ErrorAction Stop을 붙이지 않아도 catch에 들어간다
try   { [int]::Parse('abc') }
catch { Write-Warning "붙잡음: $($_.Exception.Message)" }

까다로운 점은 「파일이 없다」 같은 같은 상황이라도, 코맨드릿 쪽의 구현에 따라 비종료 에러이거나 구문 종료 에러이거나 한다는 것입니다. 작성하는 쪽이 매번 구별하는 것은 비현실적이므로, 붙잡고 싶은 줄에는 -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에도 값을 설정합니다.59
  • pwsh -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는 세션의 입력과 출력을 통째로 텍스트로 기록하는 코맨드릿으로, 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을 명시 코맨드릿은 비종료 에러가 많다. 붙잡고 싶은 줄에는 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의 값이 그대로 종료 코드가 되어, 작업 스케줄러나 모니터링에서 성공·실패를 판정할 수 있습니다.
  • 재시도는 일시적 에러 한정·상한이 있는 지수 백오프·멱등의 세 가지 원칙으로. 영구적 에러는 즉시 실패시켜 사람에게 넘깁니다.

관련 글

관련 상담 영역

합동회사 코무라소프트는 야간 배치·정형 처리 스크립트의 에러 처리와 재시도 설계 리뷰, 「실패했는데 성공으로 처리된다」「한 달에 한 번만 떨어진다」 같은 간헐적 장애의 조사, 기존 스크립트 자산의 운영 품질 개선을 다루고 있습니다.

참고 링크

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

  2. Microsoft Learn, about_Preference_Variables. $ErrorActionPreference의 기본값이 Continue라는 점, -ErrorAction 파라미터가 개별 명령에서 우선된다는 점, 설정이 스코프와 자식 스코프에 적용된다는 점, $PSNativeCommandUseErrorActionPreference의 기본값이 $false라는 점과, robocopy처럼 0이 아닌 종료 코드를 정보로 사용하는 명령에서 스크립트 블록 안에서 일시적으로 비활성화하는 예시에 대해.  2 3 4 5 6 7

  3. Microsoft Learn, What’s New in PowerShell 7.4. 실험적 기능 PSNativeCommandErrorActionPreference($PSNativeCommandUseErrorActionPreference)가 PowerShell 7.4에서 정식 기능(mainstream)이 되었다는 점에 대해.  2 3 4

  4. Microsoft Learn, Everything you wanted to know about exceptions. catch 블록 안에서 $_로부터 예외 정보에 접근할 수 있다는 점, -ErrorAction Stop을 붙인 명령과 Write-Error의 에러가 catch에서 다뤄지게 된다는 점, try/finally에 의한 리소스 해제 패턴에 대해.  2

  5. Microsoft Learn, about_Language_Keywords. exit 키워드가 종료 코드를 설정하고 $LASTEXITCODE에도 반영된다는 점, pwsh -File로 실행한 스크립트가 exit의 숫자 인수를 종료 코드로 반환한다는 점, exit 문이 없는 경우에는 정상 완료로 0·처리되지 않은 예외로 1이 된다는 점에 대해.  2 3

  6. Microsoft Learn, about_Pwsh. -File 실행 시 종료 코드가 정해지는 방식, -Command 실행에서는 0과 1 이외의 종료 코드가 1로 변환되므로 종료 코드를 유지하려면 exit $LASTEXITCODE가 필요하다는 점에 대해.  2 3 4

  7. Microsoft Learn, about_Try_Catch_Finally. try/catch/finally의 구문, 타입 지정 catch 블록과 다중 catch, finally 블록이 성공 시·에러 시에 더해 Ctrl+C에 의한 정지 시나 catch 안의 exit 시에도 실행된다는 점에 대해. 

  8. Microsoft Learn, Differences between Windows PowerShell 5.1 and PowerShell 7.x. PowerShell 7에서는 네이티브 명령이 stderr에 기록하기만 해서는 $?가 $false가 되지 않고, 0이 아닌 종료 코드일 때만 $false가 되도록 변경되었다는 점에 대해. 

  9. Microsoft Learn, about_Automatic_Variables. $LASTEXITCODE가 네이티브 프로그램이나 스크립트의 종료 코드를 보유한다는 점, pwsh -File 호출 시 예외 종료로 1·exit 키워드의 값·정상 완료로 0이 설정된다는 점에 대해. 

  10. Microsoft Learn, Start-Transcript. 세션의 명령과 콘솔 출력을 텍스트 파일로 기록한다는 점, -Append에 의한 추가 기록, 기본 저장 위치와 파일명, Stop-Transcript에 의한 정지에 대해.  2

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

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

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

자주 묻는 질문

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

PowerShell에서 try/catch를 작성했는데 catch에 들어가지 않는 이유는 무엇인가요?
코맨드릿이 내는 에러의 상당수가 비종료 에러(non-terminating error)이기 때문입니다. try/catch가 붙잡는 것은 종료 에러뿐이며, 비종료 에러는 메시지를 표시하고 처리를 계속 진행하므로 catch에는 들어가지 않습니다. 대처의 정석은 붙잡고 싶은 명령에 -ErrorAction Stop을 붙이는 것(또는 스크립트 첫머리에서 $ErrorActionPreference = 'Stop'으로 설정하는 것)입니다. 이렇게 하면 비종료 에러가 종료 에러로 승격되어 try/catch로 다룰 수 있게 됩니다.
$?와 $LASTEXITCODE는 어떻게 구분해서 사용하나요?
$?는 직전 작업이 성공했는지를 나타내는 불리언 값으로, 코맨드릿에도 네이티브 명령에도 설정됩니다. $LASTEXITCODE는 마지막으로 실행한 네이티브 프로그램(또는 exit한 스크립트)의 종료 코드로, 코맨드릿의 에러로는 변하지 않습니다. 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로 되돌리는 등의 배려가 필요합니다.

저자 프로필

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

Go Komura

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

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

블로그 목록으로 돌아가기