Share via


예외에 대해 알고 싶었던 모든 것

코드 작성에서 오류 처리는 일상적인 일입니다. 예상되는 동작에 대한 조건을 자주 확인하고 검사할 수 있습니다. 예기치 않은 일이 발생하면 예외 처리로 전환합니다. 다른 사용자의 코드에서 생성한 예외를 쉽게 처리하거나 다른 사용자가 처리할 예외를 직접 생성할 수 있습니다.

참고 항목

현재 문서의 원본 버전@KevinMarquette가 작성한 블로그에 있습니다. PowerShell 팀은 이 콘텐츠를 공유해 주신 Kevin에게 감사드립니다. PowerShellExplained.com에 있는 그의 블로그를 확인하세요.

기본 용어

이 주제를 살펴보기 전에 먼저 몇 가지 기본적인 용어를 살펴봐야 합니다.

예외

예외는 일반적인 오류 처리로는 문제를 처리할 수 없을 때 생성되는 이벤트와 비슷합니다. 예외를 생성하는 대표적인 예는 숫자를 0으로 나누려고 하거나 메모리가 부족한 상황입니다. 사용 중인 코드 작성자가 특정 문제 발생 시 예외를 생성하기도 합니다.

Throw 및 Catch

예외가 발생하면 예외가 throw됩니다. throw된 예외를 처리하려면 예외를 catch해야 합니다. 예외가 throw되고 무언가에 의해 catch되지 않으면 스크립트 실행이 중지됩니다.

호출 스택

호출 스택은 서로를 호출한 함수 목록입니다. 함수가 호출되면 스택 또는 목록의 맨 위에 추가됩니다. 함수는 종료되거나 반환되면 스택에서 제거됩니다.

예외가 throw되면 관련 호출 스택을 확인해 예외 처리기가 예외를 catch합니다.

종료 및 종료하지 않는 오류

예외는 일반적으로 종료 오류입니다. throw된 예외가 catch되거나 현재 실행이 종료됩니다. 기본적으로 종료되지 않는 오류는 Write-Error에 의해 생성되며, 예외를 throw하지 않고 출력 스트림에 오류를 추가합니다.

이 점을 강조하는 이유는 Write-Error 및 다른 종료되지 않는 오류는 catch를 트리거하지 않기 때문입니다.

예외를 삼키는 중

오류를 표시하지 않는 경우입니다. 문제 해결을 매우 어렵게 만들 수 있으므로 주의해서 이 작업을 수행합니다.

기본 명령 구문

다음은 PowerShell에서 사용되는 기본 예외 처리 구문에 대한 간략한 개요입니다.

Throw

자체 예외 이벤트를 만들기 위해 키워드(keyword) 예외를 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

쓰기 오류 -ErrorAction 중지

앞에서 Write-Error는 기본적으로 종료 오류를 throw하지 않는다고 했죠. -ErrorAction Stop을 지정하면 Write-Errorcatch로 처리할 수 있는 종료 오류를 생성합니다.

Write-Error -Message "Houston, we have a problem." -ErrorAction Stop

이런 식으로 사용하는 -ErrorAction Stop 것에 대해 상기시켜 주신 리 데일리에게 감사드립니다.

Cmdlet -ErrorAction Stop

고급 함수 또는 cmdlet에서 지정 -ErrorAction Stop 하면 모든 Write-Error 문이 실행을 중지하거나 에 의해 catch처리될 수 있는 종료 오류로 바뀝니다.

Start-Something -ErrorAction Stop

ErrorAction 매개 변수에 대한 자세한 내용은 about_CommonParameters 참조하세요. 변수에 대한 $ErrorActionPreference 자세한 내용은 about_Preference_Variables 참조하세요.

Try/Catch

예외 처리가 PowerShell(및 다른 많은 언어)에서 작동하는 방식은 먼저 try 코드 섹션을 만들고 오류를 throw하는 경우 이를 수행할 수 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 블록에서 $_ 변수를 사용하여 예외 정보에 액세스할 수 있습니다.

Try/Finally

경우에 따라 오류를 처리할 필요가 없지만 예외가 발생하는 경우 실행할 코드가 필요합니다. 스크립트는 finally 이를 정확히 수행합니다.

다음 예제를 살펴보세요.

$command = [System.Data.SqlClient.SqlCommand]::New(queryString, connection)
$command.Connection.Open()
$command.ExecuteNonQuery()
$command.Connection.Close()

리소스를 열거나 연결할 때마다 리소스를 닫아야 합니다. 예외가 ExecuteNonQuery() throw되면 연결이 닫혀 있지 않습니다. 다음은 블록 내의 동일한 코드입니다 try/finally .

$command = [System.Data.SqlClient.SqlCommand]::New(queryString, connection)
try
{
    $command.Connection.Open()
    $command.ExecuteNonQuery()
}
finally
{
    $command.Connection.Close()
}

이 예제에서는 오류가 발생하면 연결이 닫힙니다. 오류가 없는 경우에도 닫힙니다. 스크립트는 finally 매번 실행됩니다.

여러분이 예외를 catch하지 않기 때문에 여전히 호출 스택으로 전파됩니다.

Try/Catch/Finally

함께 사용하는 catch 것은 완벽하게 유효합니다 finally . 대부분의 경우 하나 또는 다른 하나를 사용하지만 둘 다 사용하는 시나리오를 찾을 수 있습니다.

$PSItem

이제 기본 사항을 살펴보게 되었으므로 좀 더 깊이 파고들 수 있습니다.

catch 블록 내에는 예외에 대한 세부 정보가 포함된 형식 ErrorRecord 의 자동 변수($PSItem또는$_)가 있습니다. 다음은 몇 가지 주요 속성에 대한 간략한 개요입니다.

예제에서는 이 예외를 생성하기 위해 ReadAllText에 잘못된 경로를 사용했습니다.

[System.IO.File]::ReadAllText( '\\test\no\filefound.log')

PSItem.ToString()

이렇게 하면 로깅 및 일반 출력에 사용할 클린 가장 많은 메시지가 표시됩니다. ToString() 는 문자열 내에 배치되는 경우 $PSItem 자동으로 호출됩니다.

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

여기서 중요한 세부 정보는 코드, Line 호출이 ScriptLineNumber 시작된 위치를 보여 ScriptName줍니다.

$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

Throw된 실제 예외입니다.

$PSItem.Exception.Message

예외를 설명하는 일반적인 메시지이며 문제 해결 시 좋은 시작점입니다. 대부분의 예외에는 기본 메시지가 있지만 예외가 throw될 때 사용자 지정 항목으로 설정할 수도 있습니다.

PS> $PSItem.Exception.Message

Exception calling "ReadAllText" with "1" argument(s): "The network path was not found."

에 집합이 하나도 없는 경우 호출 $PSItem.ToString() 할 때 반환되는 메시지이기도 합니다 ErrorRecord.

$PSItem.Exception.InnerException

예외에는 내부 예외가 포함될 수 있습니다. 호출하는 코드가 예외를 catch하고 다른 예외를 throw하는 경우가 종종 있습니다. 원래 예외는 새 예외 내에 배치됩니다.

PS> $PSItem.Exception.InnerExceptionMessage
The network path was not found.

나중에 예외를 다시 throw하는 방법에 대해 이야기할 때 이 내용을 다시 살펴보겠습니다.

$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 함수를 직접 호출했기 때문에 이 예제에서 이것밖에 볼 수 없습니다. 일반적으로 스택 추적을 확인할 때는 코드가 중단된 지점과 시스템 호출이 시작된 지점을 확인합니다.

예외 작업

기본 구문 및 예외 속성보다 예외에 더 많은 것이 있습니다.

형식화된 예외 catch

catch하는 예외를 선택적으로 사용할 수 있습니다. 예외에는 형식이 있으며 catch하려는 예외 유형을 지정할 수 있습니다.

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 문을 사용하여 여러 예외 형식을 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 이 추가를 제안해 주셔서 감사합니다.

형식화된 예외 throw

PowerShell에서 형식화된 예외를 throw할 수 있습니다. 문자열을 사용하여 throw를 호출하는 대신

throw "Could not find: $path"

다음과 같은 예외 액셀러레이터를 사용해야 합니다.

throw [System.IO.FileNotFoundException] "Could not find: $path"

하지만 이 방법을 사용할 때는 메시지를 지정해야 합니다.

throw할 예외의 새 인스턴스를 만들 수도 있습니다. 시스템에 모든 기본 제공 예외에 대한 기본 메시지가 있기 때문에 이 작업을 수행하는 경우 메시지는 선택 사항입니다.

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할 수 있습니다.

catch [System.IO.FileNotFoundException]
{
    Write-Log $PSItem.ToString()
}

.NET 예외 종합 목록

이 게시물을 보완하기 위해 수백 개의 .NET 예외가 포함된 Reddit/r/PowerShell 커뮤니티의 도움을 받아 마스터 목록을 컴파일했습니다.

먼저 해당 목록에서 내 상황에 적합한 것처럼 느껴지는 예외를 검색합니다. 기본 System 네임스페이스에서 예외를 사용해야 합니다.

예외는 개체입니다.

많은 형식의 예외를 사용하기 시작하는 경우 개체임을 기억하세요. 예외에 따라 생성자와 속성이 다릅니다. FileNotFoundException 설명서를 System.IO.FileNotFoundException살펴보면 메시지와 파일 경로를 전달할 수 있습니다.

[System.IO.FileNotFoundException]::new("Could not find file", $path)

그리고 해당 FileName 파일 경로를 노출하는 속성이 있습니다.

catch [System.IO.FileNotFoundException]
{
    Write-Output $PSItem.Exception.FileName
}

다른 생성자 및 개체 속성은 .NET 설명서를 참조해야 합니다.

예외 다시 throw

블록에서 catch 수행할 모든 작업이 동일한 예외인 throw 경우 그렇지 않습니다 catch . 이 경우 일부 작업을 처리하거나 수행하려는 예외만 catch 있으면 됩니다.

예외에 대해 작업을 수행하지만 다운스트림에서 처리할 수 있도록 예외를 다시 throw하려는 경우가 있습니다. 메시지를 작성하거나 문제를 발견한 위치에 가깝게 기록할 수 있지만 스택에서 문제를 추가로 처리할 수 있습니다.

catch
{
    Write-Log $PSItem.ToString()
    throw $PSItem
}

흥미롭게도 내부에서 호출 throwcatch 할 수 있으며 현재 예외를 다시 throw합니다.

catch
{
    Write-Log $PSItem.ToString()
    throw
}

원본 스크립트 및 줄 번호와 같은 원래 실행 정보를 유지하기 위해 예외를 다시 throw하려고 합니다. 이 시점에서 새 예외를 throw하면 예외가 시작된 위치가 표시되지 않습니다.

새 예외 다시 throw

예외를 catch하지만 다른 예외를 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 방법을 보시겠습니까? 이는 사용자에게 유용한 것을 알려줍니다.

이 때문에 $PSItemErrorRecord, 우리는 또한 다시 throw하는이 방법을 사용할 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것입니다.

제가 직접 겪은 적은 없지만 이 문제는 반드시 유념해야 하는 코너 케이스입니다.

$PSCmdlet.ThrowTerminatingError() inside try/catch

한 가지 미묘한 차이 $PSCmdlet.ThrowTerminatingError() 는 Cmdlet 내에서 종료 오류를 만들지만 Cmdlet을 떠난 후에 종료되지 않는 오류로 변한다는 것입니다. 이렇게 하면 함수 호출자가 오류를 처리하는 방법을 결정해야 하는 부담이 남습니다. -ErrorAction Stop을 이용하거나 try{...}catch{...} 내에서 호출하면 다시 종료 오류로 만들 수 있습니다.

공용 함수 템플릿

커크 먼로와의 대화를 마지막으로 한 가지 방법은 그가 모든 beginprocess 것을 배치 try{...}catch{...} 하고 end 그의 모든 고급 기능을 차단한다는 것이었습니다. 이러한 일반적인 catch 블록에서, 그는 자신의 기능을 떠나 모든 예외를 처리하는 데 사용하는 $PSCmdlet.ThrowTerminatingError($PSItem) 한 줄이 있습니다.

function Start-Something
{
    [CmdletBinding()]
    param()

    process
    {
        try
        {
            ...
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($PSItem)
        }
    }
}

모든 것이 try 그의 기능 내에서 진술에 있기 때문에 모든 것이 일관되게 작동합니다. 이렇게 하면 생성된 오류의 내부 코드가 표시되지 않는 깔끔한 오류가 최종 사용자가 전달되는 효과도 있습니다.

트랩

나는 예외의 try/catch 측면에 초점을 맞췄다. 그러나 이를 마무리하기 전에 멘션 해야 하는 하나의 레거시 기능이 있습니다.

A trap 는 해당 범위에서 발생하는 모든 예외를 catch하기 위해 스크립트 또는 함수에 배치됩니다. 예외가 발생하면 해당 코드가 trap 실행되고 일반 코드가 계속됩니다. 여러 예외가 발생하면 트랩이 반복해서 호출됩니다.

trap
{
    Write-Log $PSItem.ToString()
}

throw [System.Exception]::new('first')
throw [System.Exception]::new('second')
throw [System.Exception]::new('third')

나는 개인적으로이 방법을 채택하지 않았지만 모든 예외를 기록하는 관리자 또는 컨트롤러 스크립트에서 값을 볼 수 있으며 계속 실행됩니다.

맺음말

스크립트에 적절한 예외 처리를 추가하면 더 안정적일 뿐만 아니라 이러한 예외 문제를 더 쉽게 해결할 수 있습니다.

예외 처리에 대해 이야기할 때 핵심 개념이기 때문에 많은 시간을 얘 throw 기했습니다. 또한 PowerShell을 이용하면 Write-Errorthrow를 사용하는 모든 상황을 처리할 수 있습니다. 따라서 이 문서를 읽은 후 사용해야 throw 한다고 생각하지 마세요.

이제 이 세부 정보에서 예외 처리에 대해 작성하는 데 시간이 걸렸으므로 코드에서 오류를 생성하기 위해 사용 Write-Error -Stop 으로 전환하겠습니다. 나는 또한 Kirk의 조언을 받아 모든 기능에 대한 내 goto 예외 처리기를 만들 ThrowTerminatingError 것입니다.