오류 처리는 코드 작성과 관련하여 삶의 일부일 뿐입니다. 예상 동작에 대한 조건을 확인하고 유효성을 검사할 수 있습니다. 예기치 않은 일이 발생하면 예외 처리로 전환합니다. 다른 사용자의 코드에서 생성된 예외를 쉽게 처리하거나 다른 사용자가 처리할 수 있도록 고유한 예외를 생성할 수 있습니다.
비고
이 문서의 원래 버전@KevinMarquette작성한 블로그에 나타났습니다. PowerShell 팀은 이 콘텐츠를 공유해 주신 Kevin에게 감사드립니다. PowerShellExplained.com자신의 블로그를 확인하세요.
기본 용어
이 용어로 전환하기 전에 몇 가지 기본 용어를 다루어야 합니다.
예외
예외는 일반적인 오류 처리에서 문제를 처리할 수 없을 때 생성되는 이벤트와 같습니다. 숫자를 0으로 나누거나 메모리가 부족한 경우 예외를 만드는 예제입니다. 사용 중인 코드 작성자가 특정 문제가 발생할 때 예외를 만드는 경우가 있습니다.
Throw와 Catch
예외가 발생하면 예외가 던져집니다. 던져진 예외를 처리하려면 포착해야 합니다. 예외가 발생하고 아무도 그것을 처리하지 않으면 스크립트 실행이 중지됩니다.
호출 스택
호출 스택은 서로를 호출한 함수 목록입니다. 함수가 호출되면 스택 또는 목록의 맨 위에 추가됩니다. 함수가 종료되거나 반환되면 스택에서 제거됩니다.
예외가 throw되면 예외 처리기가 이를 catch하기 위해 해당 호출 스택이 검사됩니다.
종료 및 종료하지 않는 오류
예외는 일반적으로 종료 오류입니다. 던져진 예외는 포착되거나 현재 실행을 종료시킵니다. 기본적으로 종료되지 않는 오류는 Write-Error
의해 생성되며 예외를 throw하지 않고 출력 스트림에 오류를 추가합니다.
Write-Error
및 기타 종료되지 않는 오류는 catch
트리거하지 않기 때문에 이 점을 지적합니다.
예외를 삼키는 중
오류를 포착한 후에 억제하는 경우입니다. 문제 해결을 매우 어렵게 만들 수 있으므로 주의해서 이 작업을 수행합니다.
기본 명령 구문
다음은 PowerShell에서 사용되는 기본 예외 처리 구문에 대한 간략한 개요입니다.
던지다
자체 예외 이벤트를 만들기 위해 throw
키워드를 사용하여 예외를 throw합니다.
function Start-Something
{
throw "Bad thing happened"
}
이렇게 하면 종료 오류인 런타임 예외가 생성됩니다. 호출 함수의 catch
의해 처리되거나 다음과 같은 메시지와 함께 스크립트를 종료합니다.
PS> Start-Something
Bad thing happened
At line:1 char:1
+ throw "Bad thing happened"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Bad thing happened:String) [], RuntimeException
+ FullyQualifiedErrorId : Bad thing happened
Write-Error -ErrorAction 중지
나는 Write-Error
가 기본적으로 종료 오류를 발생시키지 않는 것을 언급했다.
-ErrorAction Stop
을 지정하면 Write-Error
이 생성하는 종료 오류는 catch
로 처리할 수 있습니다.
Write-Error -Message "Houston, we have a problem." -ErrorAction Stop
이런 식으로 -ErrorAction Stop
사용하는 것에 대해 상기시켜 주신 리 데일리에게 감사드립니다.
cmdlet -ErrorAction 중지
고급 함수 또는 cmdlet에서 -ErrorAction Stop
을 지정하면, 모든 Write-Error
문이 종료 오류로 바뀌어 실행을 중지시킵니다. 이러한 오류는 catch
로 처리할 수 있습니다.
Start-Something -ErrorAction Stop
ErrorAction 매개 변수에 대한 자세한 내용은 about_CommonParameters참조하세요.
$ErrorActionPreference
변수에 대한 자세한 내용은 about_Preference_Variables참조하세요.
예외 처리 (Try/Catch)
예외 처리가 PowerShell(및 기타 여러 언어)에서 작동하는 방식은 먼저 코드 섹션을 try
오류가 발생하는 경우 catch
수 있다는 것입니다. 다음은 빠른 샘플입니다.
try
{
Start-Something
}
catch
{
Write-Output "Something threw an exception"
Write-Output $_
}
try
{
Start-Something -ErrorAction Stop
}
catch
{
Write-Output "Something threw an exception or used Write-Error"
Write-Output $_
}
catch
스크립트는 종료 오류가 있는 경우에만 실행됩니다.
try
올바르게 실행되면 catch
건너뜁니다.
$_
변수를 사용하여 catch
블록의 예외 정보에 액세스할 수 있습니다.
트라이/파이널리
경우에 따라 오류를 처리할 필요가 없지만 예외가 발생하는 경우 실행할 코드가 필요합니다.
finally
스크립트는 이를 정확히 수행합니다.
이 예제를 살펴보세요.
$command = [System.Data.SqlClient.SqlCommand]::new(queryString, connection)
$command.Connection.Open()
$command.ExecuteNonQuery()
$command.Connection.Close()
리소스를 열거나 연결할 때마다 리소스를 닫아야 합니다.
ExecuteNonQuery()
에서 예외가 발생하면 연결이 닫히지 않습니다. 다음은 try/finally
블록 내의 동일한 코드입니다.
$command = [System.Data.SqlClient.SqlCommand]::new(queryString, connection)
try
{
$command.Connection.Open()
$command.ExecuteNonQuery()
}
finally
{
$command.Connection.Close()
}
이 예제에서는 오류가 발생하면 연결이 닫힙니다. 오류가 없는 경우에도 닫힙니다.
finally
스크립트는 매번 실행됩니다.
예외를 캐치하지 않기 때문에 호출 스택을 통해 계속 전파됩니다.
시도(try)/포착(catch)/마침내(finally)
catch
사용하고 finally
함께 사용하는 것은 완벽하게 유효합니다. 대부분의 경우 하나 또는 다른 하나를 사용하지만 둘 다 사용하는 시나리오를 찾을 수 있습니다.
$PSItem
이제 기본 사항을 살펴보게 되었으므로 좀 더 깊이 파고들 수 있습니다.
catch
블록 내에는 예외에 대한 세부 정보가 포함된 ErrorRecord
형식의 자동 변수($PSItem
또는 $_
)가 있습니다. 다음은 몇 가지 주요 속성에 대한 간략한 개요입니다.
이러한 예제에서는 ReadAllText
잘못된 경로를 사용하여 이 예외를 생성했습니다.
[System.IO.File]::ReadAllText( '\\test\no\filefound.log')
PSItem.ToString()
이렇게 하면 로깅 및 일반 출력에 사용할 가장 깨끗한 메시지가 표시됩니다.
$PSItem
문자열 내에 배치되면 ToString()
자동으로 호출됩니다.
catch
{
Write-Output "Ran into an issue: $($PSItem.ToString())"
}
catch
{
Write-Output "Ran into an issue: $PSItem"
}
$PSItem.InvocationInfo
이 속성에는 예외가 throw된 함수 또는 스크립트에 대한 PowerShell에서 수집한 추가 정보가 포함됩니다. 여기 제가 만든 샘플 예외의 InvocationInfo
입니다.
PS> $PSItem.InvocationInfo | Format-List *
MyCommand : Get-Resource
BoundParameters : {}
UnboundArguments : {}
ScriptLineNumber : 5
OffsetInLine : 5
ScriptName : C:\blog\throwerror.ps1
Line : Get-Resource
PositionMessage : At C:\blog\throwerror.ps1:5 char:5
+ Get-Resource
+ ~~~~~~~~~~~~
PSScriptRoot : C:\blog
PSCommandPath : C:\blog\throwerror.ps1
InvocationName : Get-Resource
여기서 중요한 세부 정보는 ScriptName
, 코드의 Line
및 호출이 시작된 위치의 ScriptLineNumber
를 보여줍니다.
$PSItem.ScriptStackTrace (스크립트 스택 트레이스)
이 속성은 예외가 생성된 코드로 연결한 함수 호출의 순서를 보여 있습니다.
PS> $PSItem.ScriptStackTrace
at Get-Resource, C:\blog\throwerror.ps1: line 13
at Start-Something, C:\blog\throwerror.ps1: line 5
at <ScriptBlock>, C:\blog\throwerror.ps1: line 18
동일한 스크립트에서 함수만 호출하지만 여러 스크립트가 관련된 경우 호출을 추적합니다.
$PSItem.Exception
이 예외는 실제로 발생한 것입니다.
$PSItem.Exception.Message (예외 메시지)
예외를 설명하는 일반적인 메시지이며 문제 해결 시 좋은 시작점입니다. 대부분의 예외에는 기본 메시지가 있지만 예외가 throw될 때 사용자 지정 항목으로 설정할 수도 있습니다.
PS> $PSItem.Exception.Message
Exception calling "ReadAllText" with "1" argument(s): "The network path was not found."
ErrorRecord
에 설정이 없을 경우, $PSItem.ToString()
호출 시 반환되는 메시지입니다.
$PSItem.Exception.InnerException
예외에는 내부 예외가 포함될 수 있습니다. 호출하는 코드가 예외를 잡고 다른 예외를 던지는 경우가 종종 있습니다. 원래 예외는 새 예외 내에 배치됩니다.
PS> $PSItem.Exception.InnerExceptionMessage
The network path was not found.
나중에 예외를 다시 던지는 것에 대해 이야기할 때 이 문제를 다시 살펴보겠습니다.
$PSItem.Exception.StackTrace
예외에 대한 StackTrace
라는 것입니다. 위의 ScriptStackTrace
을 보여 주었지만, 이번 것은 관리 코드 호출에 대한 것입니다.
at System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean
useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs,
String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean checkHost)
at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32
bufferSize, FileOptions options, String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean
checkHost)
at System.IO.StreamReader..ctor(String path, Encoding encoding, Boolean detectEncodingFromByteOrderMarks,
Int32 bufferSize, Boolean checkHost)
at System.IO.File.InternalReadAllText(String path, Encoding encoding, Boolean checkHost)
at CallSite.Target(Closure , CallSite , Type , String )
관리 코드에서 이벤트가 throw되는 경우에만 이 스택 추적을 가져옵니다. 이 예제에서 볼 수 있도록 .NET Framework 함수를 직접 호출합니다. 일반적으로 스택 추적을 볼 때 코드가 중지되고 시스템 호출이 시작되는 위치를 찾습니다.
예외 처리 작업
기본 구문 및 예외 속성보다 예외에 더 많은 것이 있습니다.
형식화된 예외 처리
선택적으로 특정 예외를 포착할 수 있습니다. 예외에는 타입이 있으며 포착하려는 예외 타입을 지정할 수 있습니다.
try
{
Start-Something -Path $path
}
catch [System.IO.FileNotFoundException]
{
Write-Output "Could not find $path"
}
catch [System.IO.IOException]
{
Write-Output "IO error with the file: $path"
}
예외 유형은 예외와 일치하는 블록이 발견될 때까지 각 catch
블록에 대해 확인됩니다.
예외가 다른 예외에서 상속할 수 있음을 깨닫는 것이 중요합니다. 위의 예제에서 FileNotFoundException
IOException
상속합니다. 만약 IOException
가 먼저였다면, 대신 호출될 것입니다. 일치하는 항목이 여러 개 있는 경우에도 하나의 catch 블록만 호출됩니다.
System.IO.PathTooLongException
이 있으면 IOException
이 일치하겠지만, InsufficientMemoryException
이 있으면 아무도 그걸 잡을 수 없어서 스택까지 전파될 것입니다.
한 번에 여러 종류를 잡다.
동일한 catch
문을 사용하여 여러 예외 형식을 catch할 수 있습니다.
try
{
Start-Something -Path $path -ErrorAction Stop
}
catch [System.IO.DirectoryNotFoundException],[System.IO.FileNotFoundException]
{
Write-Output "The path or file was not found: [$path]"
}
catch [System.IO.IOException]
{
Write-Output "IO error with the file: [$path]"
}
레딧 사용자 u/Sheppard_Ra
님께 이 추가 제안을 주셔서 감사합니다.
형식화된 예외를 던지기
PowerShell에서 형식화된 예외를 throw할 수 있습니다. 문자열을 사용하여 throw
호출하는 대신 다음을 수행합니다.
throw "Could not find: $path"
다음과 같이 예외 가속기를 사용합니다.
throw [System.IO.FileNotFoundException] "Could not find: $path"
그러나 그런 식으로 할 때는 메시지를 반드시 지정해야 합니다.
예외를 던질 새로운 인스턴스를 생성할 수도 있습니다. 시스템에 모든 기본 제공 예외에 대한 기본 메시지가 있기 때문에 이 작업을 수행하는 경우 메시지는 선택 사항입니다.
throw [System.IO.FileNotFoundException]::new()
throw [System.IO.FileNotFoundException]::new("Could not find path: $path")
PowerShell 5.0 이상을 사용하지 않는 경우 이전 New-Object
방법을 사용해야 합니다.
throw (New-Object -TypeName System.IO.FileNotFoundException )
throw (New-Object -TypeName System.IO.FileNotFoundException -ArgumentList "Could not find path: $path")
형식화된 예외를 사용하면 이전 섹션에서 설명한 대로 사용자(또는 다른 사람)가 형식별로 예외를 catch할 수 있습니다.
Write-Error -Exception
이러한 유형의 예외를 Write-Error
추가할 수 있으며, 우리는 여전히 예외 유형별로 오류를 catch
정리할 수 있습니다. 다음 예제와 같이 Write-Error
사용합니다.
# with normal message
Write-Error -Message "Could not find path: $path" -Exception ([System.IO.FileNotFoundException]::new()) -ErrorAction Stop
# With message inside new exception
Write-Error -Exception ([System.IO.FileNotFoundException]::new("Could not find path: $path")) -ErrorAction Stop
# Pre PS 5.0
Write-Error -Exception ([System.IO.FileNotFoundException]"Could not find path: $path") -ErrorAction Stop
Write-Error -Message "Could not find path: $path" -Exception (New-Object -TypeName System.IO.FileNotFoundException) -ErrorAction Stop
그런 다음 이렇게 잡을 수 있습니다.
catch [System.IO.FileNotFoundException]
{
Write-Log $PSItem.ToString()
}
.NET 예외의 큰 목록
이 게시물을 보완하기 위해 수백 개의 .NET 예외를 포함하는 Reddit r/PowerShell
커뮤니티의 도움으로 마스터 목록을 컴파일했습니다.
먼저 해당 목록에서 내 상황에 적합한 것처럼 느껴지는 예외를 검색합니다. 기본 System
네임스페이스에서 예외를 사용해야 합니다.
예외는 개체입니다.
많은 형식의 예외를 사용하기 시작하는 경우 개체임을 기억하세요. 예외에 따라 생성자와 속성이 다릅니다.
System.IO.FileNotFoundException
대한 FileNotFoundException 설명서를 살펴보면 메시지와 파일 경로를 전달할 수 있습니다.
[System.IO.FileNotFoundException]::new("Could not find file", $path)
또한 해당 파일 경로를 노출하는 FileName
속성이 있습니다.
catch [System.IO.FileNotFoundException]
{
Write-Output $PSItem.Exception.FileName
}
다른 생성자 및 개체 속성에 대한 .NET 설명서 참조해야 합니다.
예외를 다시 던지기
catch
블록에서 동일한 예외를 throw
는 것만 하게 된다면, 그것을 catch
지 마세요. 발생했을 때 처리하거나 조치를 취할 계획인 예외만 catch
하십시오.
예외에 대해 작업을 수행하지만 다운스트림에서 처리할 수 있도록 예외를 다시 throw하려는 경우가 있습니다. 메시지를 작성하거나 문제를 발견한 위치에 가까운 곳에서 문제를 기록할 수 있지만, 상위 스택에서 문제를 처리할 수 있습니다.
catch
{
Write-Log $PSItem.ToString()
throw $PSItem
}
흥미롭게도 catch
내에서 throw
을 호출할 수 있으며 현재 예외를 다시 발생시킵니다.
catch
{
Write-Log $PSItem.ToString()
throw
}
원본 스크립트 및 줄 번호와 같은 원래 실행 정보를 유지하기 위해 예외를 다시 throw하려고 합니다. 이 시점에서 새 예외를 throw하면 예외가 시작된 위치가 숨겨집니다.
새로운 예외 다시 발생시키기
예외를 잡았지만 다른 예외를 발생시키고자 한다면, 원래 예외를 새로운 예외에 포함시켜야 합니다. 이렇게 하면 계층 구조에서 아래에 있는 누군가가 $PSItem.Exception.InnerException
으로 액세스할 수 있습니다.
catch
{
throw [System.MissingFieldException]::new('Could not access field',$PSItem.Exception)
}
$PSCmdlet.ThrowTerminatingError()
원시 예외에 throw
사용하는 것을 좋아하지 않는 한 가지는 오류 메시지가 throw
문을 가리키고 해당 줄이 문제가 있는 위치를 나타낸다는 것입니다.
Unable to find the specified file.
At line:31 char:9
+ throw [System.IO.FileNotFoundException]::new()
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (:) [], FileNotFoundException
+ FullyQualifiedErrorId : Unable to find the specified file.
31줄에서 throw
을 호출했기 때문에 스크립트에 오류가 있다는 메시지가 나타나는 것은 스크립트 사용자에게 좋지 않은 경험입니다. 그것은 그들에게 유용한 아무것도 말하지 않습니다.
덱스터 다미는 내가 그것을 해결하기 위해 ThrowTerminatingError()
사용할 수 있다고 지적했다.
$PSCmdlet.ThrowTerminatingError(
[System.Management.Automation.ErrorRecord]::new(
([System.IO.FileNotFoundException]"Could not find $Path"),
'My.ID',
[System.Management.Automation.ErrorCategory]::OpenError,
$MyObject
)
)
Get-Resource
함수 내에서 ThrowTerminatingError()
호출되었다고 가정하면 이것이 표시되는 오류입니다.
Get-Resource : Could not find C:\Program Files (x86)\Reference
Assemblies\Microsoft\Framework\.NETPortable\v4.6\System.IO.xml
At line:6 char:5
+ Get-Resource -Path $Path
+ ~~~~~~~~~~~~
+ CategoryInfo : OpenError: (:) [Get-Resource], FileNotFoundException
+ FullyQualifiedErrorId : My.ID,Get-Resource
문제의 원인으로 Get-Resource
함수를 가리키는 방법을 보시겠습니까? 이는 사용자에게 유용한 것을 알려줍니다.
$PSItem
이 ErrorRecord
이므로, 이 방법으로 ThrowTerminatingError
를 재활용할 수 있습니다.
catch
{
$PSCmdlet.ThrowTerminatingError($PSItem)
}
이렇게 하면 오류의 원본이 Cmdlet으로 변경되고 Cmdlet의 사용자로부터 함수 내부가 숨겨지게 됩니다.
시도하면 종료 오류를 생성할 수 있습니다.
Kirk Munro는 일부 예외가 try/catch
블록 내에서 실행될 때만 오류를 종료한다고 지적합니다. 그가 나에게 준 예시는 런타임 예외로 0으로 나누기를 발생시키는 것입니다.
function Start-Something { 1/(1-1) }
그런 다음 다음과 같이 호출하여 오류를 생성하고 메시지를 출력하는 것을 확인합니다.
&{ Start-Something; Write-Output "We did it. Send Email" }
그러나 동일한 코드를 try/catch
내부에 배치하면 다른 문제가 발생하는 것을 볼 수 있습니다.
try
{
&{ Start-Something; Write-Output "We did it. Send Email" }
}
catch
{
Write-Output "Notify Admin to fix error and send email"
}
오류가 종료 오류가 되고 첫 번째 메시지를 출력하지 않는 것을 볼 수 있습니다. 이 코드에 대해 마음에 들지 않는 것은 함수에 이 코드를 사용할 수 있고 다른 사용자가 try/catch
사용하는 경우 다르게 작동한다는 것입니다.
나는 직접 이 문제를 겪지는 않았지만, 주의해야 할 특이 케이스입니다.
try/catch 내부에서 "$PSCmdlet.ThrowTerminatingError()"가 실행됨
$PSCmdlet.ThrowTerminatingError()
한 가지 미묘한 차이는 Cmdlet 내에서 종료 오류를 생성하지만 Cmdlet을 떠난 후 종료되지 않는 오류로 변한다는 것입니다. 이렇게 하면 함수 호출자가 오류를 처리하는 방법을 결정해야 하는 부담이 남습니다.
-ErrorAction Stop
사용하거나 try{...}catch{...}
내에서 호출하여 종료 오류로 되돌릴 수 있습니다.
공용 함수 템플릿
마지막으로 커크 먼로(Kirk Munro)와의 대화에서 얻은 교훈은 그가 모든 고급 기능에서 begin
, process
, end
블록 주위에 try{...}catch{...}
을 배치한다는 것이었습니다. 이러한 일반적인 catch 블록에서, 그는 자신의 함수에서 발생하는 모든 예외를 처리하기 위해 $PSCmdlet.ThrowTerminatingError($PSItem)
을 사용하여 한 줄을 포함하고 있습니다.
function Start-Something
{
[CmdletBinding()]
param()
process
{
try
{
...
}
catch
{
$PSCmdlet.ThrowTerminatingError($PSItem)
}
}
}
그의 함수 내에서 모든 것이 try
문에 있기 때문에 일관되게 작동합니다. 최종 사용자에게 명확한 오류 메시지를 제공하여 생성된 오류에서 내부 코드를 숨깁니다.
함정
나는 예외의 try/catch
측면에 초점을 맞췄다. 그러나 이것을 마무리하기 전에 언급해야 할 하나의 레거시 기능이 있습니다.
스크립트 또는 함수에 trap
가 모든 예외를 포착하기 위해 해당 범위에 배치됩니다. 예외가 발생하면 trap
코드가 실행된 후 일반 코드가 계속됩니다. 여러 예외가 발생하면 트랩이 반복해서 호출됩니다.
trap
{
Write-Log $PSItem.ToString()
}
throw [System.Exception]::new('first')
throw [System.Exception]::new('second')
throw [System.Exception]::new('third')
나는 개인적으로이 방법을 채택하지 않았지만 모든 예외를 기록하는 관리자 또는 컨트롤러 스크립트에서 값을 볼 수 있으며 계속 실행됩니다.
닫는 설명
스크립트에 적절한 예외 처리를 추가하면 더 안정적일 뿐만 아니라 이러한 예외 문제를 더 쉽게 해결할 수 있습니다.
예외 처리에 대해 이야기할 때 핵심 개념이기 때문에 throw
이야기하는 데 많은 시간을 보냈습니다. PowerShell은 또한 우리가 throw
을 사용할 모든 상황을 처리할 수 있는 Write-Error
을 제공했습니다. 따라서 이 문서를 읽은 후 throw
사용해야 한다고 생각하지 마세요.
이제 예외 처리에 대해 자세하게 작성했으니, 코드에서 오류를 생성하기 위해 Write-Error -Stop
을 사용하는 방식으로 전환하려고 합니다. 저는 Kirk의 조언에 따라 모든 함수의 예외 처리를 위한 기본 핸들러로 ThrowTerminatingError
을 이용하려고 합니다.
PowerShell