about_Error_Handling

about_Error_Handling (오류 처리)

PowerShell에서 발생하는 오류의 종류와 오류를 처리하는 메커니즘을 설명해 드릴게요.

출처: Microsoft Learn — about_Error_Handling https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_error_handling

본문

간단한 설명 (Short description)

PowerShell에서 발생하는 오류의 종류와, 그 오류를 처리하는 메커니즘에 대해 설명드릴게요.

자세한 설명 (Long description)

PowerShell은 오류를 크게 세 가지 범주로 구분해요.

  • 비종료 오류 (Non-terminating errors)
  • 문 종료 오류 (Statement-terminating errors)
  • 스크립트 종료 오류 (Script-terminating errors)

이 차이를 이해하는 게 안정적인 스크립트와 모듈을 작성하는 데 아주 중요해요. 왜냐하면 각 범주마다 기본 동작이 다르고, 처리하는 방식도 달라야 하기 때문이에요.

여기에 더해서, 외부(네이티브) 프로그램은 종료 코드(exit code)로 실패를 알려줘요. PowerShell은 이 값을 자체 오류 체계와는 별도로 추적한답니다.

오류의 종류 (Types of errors)

비종료 오류 (Non-terminating errors)

비종료 오류는 문제를 보고하지만 파이프라인을 멈추지 않아요. 명령은 이후에 오는 입력 개체들을 계속 처리하지요. 비종료 오류는 이런 경우에 생겨요.

  • Write-Error cmdlet
  • 고급 함수에서의 $PSCmdlet.WriteError() 메서드
  • 개별 입력 개체에서 복구 가능한 실패가 발생한 cmdlet

기본적으로 PowerShell은 오류 메시지를 화면에 보여주고 실행을 계속 진행해요.

# Non-terminating error: the pipeline continues after the failure
'file1.txt', 'noSuchFile.txt', 'file3.txt' | ForEach-Object {
    Get-Content $_ -ErrorAction Continue
}

이 예시에서 Get-ContentnoSuchFile.txt에 대해 비종료 오류를 보고한 다음, file3.txt를 계속 처리해요.

비종료 오류는 기본적으로 catchtrap촉발하지 않아요.

문 종료 오류 (Statement-terminating errors)

문 종료 오류는 현재 문(파이프라인)이 실행되는 걸 멈추지만, 스크립트의 다음 문은 이어서 실행돼요. 문 종료 오류는 이런 경우에 생겨요.

  • 고급 함수와 컴파일된 cmdlet에서의 $PSCmdlet.ThrowTerminatingError() 메서드
  • CommandNotFoundException(존재하지 않는 명령 호출), ParameterBindingException(잘못된 매개 변수 인수) 같은 엔진 오류
  • 예외를 던지는 .NET 메서드 호출, 예를 들어 [int]::Parse('abc')
# Statement-terminating error: Get-Item fails, but the next statement runs
Get-Item -Path 'C:\NoSuchFile.txt'
Write-Output 'This still runs'

문 종료 오류는 try/catchtrap으로 잡아낼 수 있어요.

.ThrowTerminatingError()-ErrorAction 매개 변수를 참고하지 않아요(Break 값은 예외인데, 그건 디버거로 진입시키죠). 다만 $ErrorActionPreference는 엔진의 문 수준 핸들러를 통해 문 종료 오류에도 적용돼요. 예를 들어 $ErrorActionPreference = 'SilentlyContinue'로 설정하면 문 종료 오류를 억제해서 스크립트가 다음 문에서 이어지게 할 수 있어요. -ErrorAction 매개 변수는 이렇게 할 수 없어요. 자세한 내용은 The $ErrorActionPreference asymmetry를 참고해 주세요.

스크립트 종료 오류 (Script-terminating errors)

스크립트 종료 오류는 전체 호출 스택을 풀어버려요. try/catch 블록이나 trap 문으로 오류를 잡지 않는 한, 실행이 완전히 멈추지요. 스크립트 종료 오류는 이런 경우에 생겨요.

  • throw 키워드
  • 구문 분석 오류(스크립트를 컴파일하지 못하게 하는 구문 오류)
  • 비고급 컨텍스트에서 -ErrorAction Stop 또는 $ErrorActionPreference = 'Stop'에 의해 에스컬레이션된 비종료 오류. 자세한 내용은 How escalation works를 참고해 주세요.
  • 특정한 중요한 엔진 실패
# Script-terminating error: throw unwinds the call stack
function Test-Throw {
    throw 'Critical failure'
    Write-Output 'This never runs'
}

Test-Throw
Write-Output 'This never runs either (unless caught)'

throw 키워드는 기본적으로 스크립트 종료 오류를 만들어요. 하지만 $ErrorActionPreferenceSilentlyContinueIgnore로 설정되어 있으면 throw를 억제할 수도 있어요. 고급 함수를 -ErrorAction SilentlyContinue로 호출하면, 그 매개 변수가 스코프-로컬 $ErrorActionPreference 값으로 변환되기 때문에 그 함수 안의 throw도 억제되요.

$ErrorActionPreference = 'Ignore'로 설정해도 억제된 throw는 여전히 $Error에 항목을 기록해요. Ignore 값이 $Error 기록을 막는 건 비종료 오류에 한해서예요.

문 종료스크립트 종료라는 용어는 오류의 심각도를 뜻하는 게 아니라 영향 범위를 뜻해요. 문 종료 오류는 하나의 문을 멈추고, 스크립트 종료 오류는 스크립트 전체와 그 호출자를 멈추지요. 둘 다 try/catch로 잡을 수 있어요.

외부 프로그램 오류 (External program errors)

외부(네이티브) 프로그램은 PowerShell의 오류 체계에 직접 참여하지 않아요. 그들은 0이 아닌 종료 코드로 실패를 알리고, PowerShell은 그 값을 $LASTEXITCODE 자동 변수에 저장해요.

git clone https://example.com/nonexistent.git 2>$null
if ($LASTEXITCODE -ne 0) {
    Write-Error "git failed with exit code $LASTEXITCODE"
}

기본적으로 네이티브 프로그램의 0이 아닌 종료 코드는:

  • $?$false로 설정해요
  • $ErrorErrorRecord만들지 않아요
  • catchtrap촉발하지 않아요

PowerShell 7.3은 실험용 기본 설정 변수 $PSNativeCommandUseErrorActionPreference를 추가했고, 이건 7.4에서 안정 기능이 됐어요. 이 변수를 $true로 설정하면 0이 아닌 종료 코드가 특정 종료 코드를 알려주는 비종료 오류(NativeCommandExitException)를 발생시켜요. 이 오류는 $ErrorActionPreference를 따르기 때문에, 이걸 Stop으로 설정하면 오류가 스크립트 종료 오류로 승격되어 try/catch로 잡을 수 있답니다.

오류 상태 변수 (Error state variables)

PowerShell은 현재 오류 상태를 반영하는 여러 자동 변수를 유지해요.

$?

마지막 작업이 성공하면 $true, 오류(비종료든 종료든)를 만들었다면 $false를 담아요. 네이티브 명령의 경우 $?는 종료 코드를 기준으로 설정돼요. 종료 코드 0이면 $true, 그 외에는 $false예요.

Get-Item -Path 'C:\NoSuchFile.txt' 2>$null
$?  # False

$Error

가장 최근의 오류 기록을 저장하는 ArrayList로, 인덱스 0이 가장 최신 오류예요. 목록은 $MaximumErrorCount개(기본 256개)까지 담아요.

모든 종료 오류는 $Error에 추가돼요. 종료 오류의 경우 Ignore는 표시는 억제하지만 여전히 $Error에는 기록해요. 모든 비종료 오류도 $Error에 추가되지만, 비종료 오류에 -ErrorAction Ignore를 사용하면 표시와 기록을 모두 막아요.

$LASTEXITCODE

마지막으로 실행된 네이티브 프로그램의 종료 코드를 담아요. 0이면 관례적으로 성공, 0이 아니면 실패를 뜻해요. 이 변수는 PowerShell cmdlet 오류의 영향을 받지 않아요.

오류 동작 제어 (Control error behavior)

-ErrorAction 공용 매개 변수

-ErrorAction 공용 매개 변수는 단일 명령에 대해 $ErrorActionPreference를 덮어써요. 그 명령에서 발생하는 비종료 오류에 PowerShell이 어떻게 반응할지를 제어하지요.

-ErrorAction$PSCmdlet.ThrowTerminatingError()가 만들어낸 오류의 동작을 바꾸지 않아요. 그런 오류는 호출자의 기본 설정과 무관하게 항상 문 종료 오류이에요.

$ErrorActionPreference 변수

$ErrorActionPreference 기본 설정 변수는 현재 스코프와 하위 스코프의 모든 명령에 적용돼요. -ErrorAction과 같은 값을 받아들여요.

$ErrorActionPreference = 'Stop'
# All non-terminating errors in this scope now become terminating
Write-Error 'This now throws'   # Generates ActionPreferenceStopException

명령에 -ErrorAction을 지정하면, 그 명령에 한해서 $ErrorActionPreference보다 우선해요.

에스컬레이션이 동작하는 방식 (How escalation works)

-ErrorAction Stop이나 $ErrorActionPreference = 'Stop'이 적용 중이면, PowerShell은 다음과 같은 메커니즘으로 비종료 오류를 종료 오류로 변환해요.

  1. cmdlet이 내부적으로 WriteError()를 호출해 비종료 오류를 발생시켜요.
  2. 엔진이 그 명령에 적용되는 ErrorAction 기본 설정을 확인해요.
  3. 기본 설정이 Stop이므로, 엔진은 원래 오류 기록을 감싸는 ActionPreferenceStopException을 만들어요.
  4. catch로 잡으면 원래 오류 정보를 $_.Exception.ErrorRecord로 접근할 수 있어요.

에스컬레이션된 오류의 범위는 컨텍스트에 따라 달라져요.

  • 비고급 스크립트, 함수, 스크립트 블록에서는 $ErrorActionPreference = 'Stop'으로 설정하면 스크립트 종료 오류로 에스컬레이션돼요. 오류가 호출 스택을 타고 위로 전파되요.
  • 고급 함수와 스크립트 블록([CmdletBinding()]이 있는 것들)에서는 오류가 문 종료 상태로 남아요. 호출 다음의 문부터 실행이 이어지지요.
  • 고급 함수에 -ErrorAction Stop을 넘기면 그 함수 안에서 $ErrorActionPreference = 'Stop'을 설정하는 것과 같은 효과예요. -ErrorAction이 스코프-로컬 $ErrorActionPreference 값으로 변환되기 때문이에요.

에스컬레이션 예시 (Escalation examples)

  • 비고급(non-advanced): 스크립트 종료('after'는 출력되지 않아요)
& {
param()
$ErrorActionPreference = 'Stop'
1/0 # Divide by zero error
} 2>$null
'after'
  • 고급(advanced): 문 종료('after'는 출력돼요)
& {
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop'
1/0 # Divide by zero error
} 2>$null
'after'
  • -ErrorAction Stop 없이: 비종료, catch는 실행되지 않아요
try {
Write-Error 'This is non-terminating'
Write-Output 'Execution continues'
} catch {
Write-Output "Caught: $_" # Not reached
}
  • -ErrorAction Stop 사용 시: 종료로 에스컬레이션
try {
Write-Error 'This becomes terminating' -ErrorAction Stop
} catch {
Write-Output "Caught: $_" # Reached
}

에스컬레이션된 오류는 원래 예외 유형으로 잡을 수 있어요. 엔진이 ActionPreferenceStopException을 풀어서 내부 예외를 찾아내거든요.

try {
    Get-Item -Path 'C:\NoSuchFile.txt' -ErrorAction Stop
} catch [System.Management.Automation.ItemNotFoundException] {
    Write-Output "File not found: $($_.Exception.Message)"
}

$ErrorActionPreference의 비대칭성 (The $ErrorActionPreference asymmetry)

-ErrorAction 매개 변수와 $ErrorActionPreference 변수는 종료 오류를 대할 때 다르게 동작해요. 이 비대칭성을 이해하는 게 중요하답니다.

  • -ErrorAction비종료 오류에만 영향을 줘요. cmdlet이 $PSCmdlet.ThrowTerminatingError()를 호출하면 -ErrorAction 매개 변수는 무시돼요(Break는 예외인데, 디버거로 진입시키죠). 오류는 항상 던져져요.
  • $ErrorActionPreference비종료 오류와 문 종료 오류 모두에 영향을 줘요. 엔진의 문 수준 오류 핸들러가(-ErrorAction 매개 변수가 아니라) $ErrorActionPreference를 읽어서, 값이 SilentlyContinueIgnore일 때 문 종료 오류를 억제할 수 있어요.
function Test-Asymmetry {
    [CmdletBinding()]
    param()
    $er = [System.Management.Automation.ErrorRecord]::new(
        [System.InvalidOperationException]::new('test error'),
        'TestError',
        [System.Management.Automation.ErrorCategory]::InvalidOperation,
        $null
    )
    $PSCmdlet.ThrowTerminatingError($er)
}

# -ErrorAction SilentlyContinue does NOT suppress the error:
Test-Asymmetry -ErrorAction SilentlyContinue   # Error is still thrown

# $ErrorActionPreference DOES suppress the error:
$ErrorActionPreference = 'SilentlyContinue'
Test-Asymmetry   # Error is silently suppressed, script continues
$ErrorActionPreference = 'Continue'

$ErrorActionPreferenceSuppressPromptInInterpretertrue로 설정된 오류는 억제할 수 없어요. 그런 오류는 기본 설정 변수와 무관하게 항상 전파되요. 이런 유형의 예시로는:

  • -ErrorAction Stop 에스컬레이션에서 온 ActionPreferenceStopException
  • PowerShell 클래스 메서드 안의 오류
  • PipelineStoppedException

오류 처리 (Handle errors)

try/catch/finally

try/catch/finally를 사용해 문 종료 및 스크립트 종료 오류를 처리해요. try 블록 안에서 오류가 발생하면 PowerShell이 맞는 catch 블록을 찾아요. finally 블록은 오류가 발생했든 안 했든 항상 실행돼요.

try {
    $result = Get-Content -Path 'data.txt' -ErrorAction Stop
}
catch [System.Management.Automation.ItemNotFoundException] {
    Write-Warning 'Data file not found, using defaults.'
    $result = 'default'
}
catch {
    Write-Warning "Unexpected error: $_"
}
finally {
    Write-Verbose 'Cleanup complete.' -Verbose
}

try 블록 안에서 엔진은 내부 플래그를 설정해서, -ErrorAction Stop이나 $ErrorActionPreference = 'Stop'에 의해 에스컬레이션된 비종료 오류가 catch 블록으로 전파되게 해요. 이건 특별한 예외가 아니라 설계된 동작이에요.

전체 구문에 대한 자세한 내용은 about_Try_Catch_Finally를 참고해 주세요.

trap

trap 문은 스코프 수준에서 종료 오류를 처리해요. 둘러싼 스코프 어디에서든 오류가 발생하면 trap 블록이 실행돼요.

  • 기본값(breakcontinue 없음): 오류가 표시되고, 오류를 일으킨 문 다음의 문부터 실행이 이어져요.
  • trap 안의 continue: 오류 메시지를 억제하고 다음 문에서 이어져요.
  • trap 안의 break: 오류가 부모 스코프로 전파돼요.
trap [System.Management.Automation.CommandNotFoundException] {
    Write-Warning "Command not found: $($_.TargetObject)"
    continue
}

NonsenseCommand   # Trap fires, execution continues
Write-Output 'This runs because the trap used continue'

전체 구문에 대한 자세한 내용은 about_Trap를 참고해 주세요.

함수와 스크립트에서 오류 보고하기 (Reporting errors in functions and scripts)

함수와 스크립트를 작성할 때는 실패의 심각도에 맞는 오류 보고 메커니즘을 골라야 해요.

비종료 - Write-Error 사용하기

함수가 다른 입력을 계속 처리할 수 있을 때 Write-Error를 사용해요. 여러 개체를 처리하다가 개별 항목에서 실패가 발생하는 파이프라인 함수에 적합해요.

function Test-Path-Safe {
    [CmdletBinding()]
    param([Parameter(ValueFromPipeline)][string]$Path)
    process {
        if (-not (Test-Path $Path)) {
            Write-Error "Path not found: $Path"
            return
        }
        $Path
    }
}

고급 함수([CmdletBinding()]이 있는 것들)에서는 Write-Error 대신 $PSCmdlet.WriteError()를 사용해 호출자 스코프에서 $?가 제대로 $false로 설정되게 하세요. Write-Error cmdlet은 $?를 항상 올바르게 설정하지는 못해요.

문 종료 - $PSCmdlet.ThrowTerminatingError() 사용하기

함수가 아예 계속할 수 없지만, 호출자가 그 실패를 어떻게 처리할지를 결정해야 한다면 $PSCmdlet.ThrowTerminatingError()를 사용해요. 고급 함수에서 권장하는 방식이에요.

function Get-Config {
  [CmdletBinding()]
  param([string]$Path)

  if (-not (Test-Path $Path)) {
    $er = [System.Management.Automation.ErrorRecord]::new(
      [System.IO.FileNotFoundException]::new("Config file not found: $Path"),
      'ConfigNotFound',
      [System.Management.Automation.ErrorCategory]::ObjectNotFound,
      $Path
    )
    $PSCmdlet.ThrowTerminatingError($er)
  }

  Get-Content $Path | ConvertFrom-Json
}

오류가 함수를 떠난 뒤에는 호출자가 기본적으로 비종료 오류로 취급해요. 호출자는 -ErrorAction Stop으로 에스컬레이션할 수 있어요.

스크립트 종료 - throw 사용하기

복구가 불가능하고 스크립트 전체가 멈춰야 할 때는 throw를 사용해요.

$config = Get-Content 'config.json' -ErrorAction SilentlyContinue |
    ConvertFrom-Json

if (-not $config) {
    throw 'Cannot proceed without a valid configuration file.'
}

어떤 메커니즘을 쓸까요?

  • 여러 개의 입력을 처리하는데 그중 일부가 실패할 수 있다면, Write-Error$PSCmdlet.WriteError()를 사용해요.
  • 함수가 계속할 수 없다면 $PSCmdlet.ThrowTerminatingError()를 사용하고, 호출자가 처리 방식을 결정하게 해요.
  • 스크립트 전체를 즉시 멈춰야 한다면 throw를 사용해요.

오류 유형 요약 (Summary of error types)

아래 표들은 PowerShell에서 서로 다른 오류 유형의 속성과 동작을 요약해 줘요.

비종료 오류

비종료 오류(Non-terminating errors)Write-Error$PSCmdlet.WriteError()로 만들 수 있어요.

문 종료 오류

문 종료 오류(Statement-terminating errors)ThrowTerminatingError(), 엔진 오류, .NET 메서드 예외, 또는 고급 컨텍스트에서의 -ErrorAction Stop으로 만들 수 있어요.

스크립트 종료 오류

스크립트 종료 오류(Script-terminating errors)throw, 구문 분석 오류, 또는 비고급 컨텍스트에서의 -ErrorAction Stop으로 만들 수 있어요.

더 알아보기