파워셸 오류 처리 마스터하기: 간단한 가이드

PowerShell 스크립트에서 성가신 빨간 오류 메시지를 보는 것이 지겹지 않으신가요? 이 메시지들이 위협적으로 보일 수 있지만, 적절한 오류 처리는 신뢰할 수 있는 PowerShell 자동화를 구축하는 데 필수적입니다. 이 튜토리얼에서는 오류 유형을 이해하는 것부터 try/catch 블록을 마스터하는 것까지, 스크립트에서 강력한 오류 처리를 구현하는 방법을 배우게 됩니다.

전제 조건

이 튜토리얼은 다음을 가정합니다:

  • Windows PowerShell 5.1 또는 PowerShell 7+ 설치됨
  • PowerShell 스크립팅에 대한 기본적인 이해
  • 오류를 학습 기회로 받아들일 의지!

PowerShell 오류 유형 이해하기

오류 처리를 다루기 전에, PowerShell이 발생시킬 수 있는 두 가지 주요 오류 유형을 이해해야 합니다:

종료 오류

이것들은 심각한 오류입니다 – 스크립트 실행을 완전히 중단시키는 오류입니다. 종료 오류는 다음과 같은 경우에 발생합니다:

  • 스크립트에 구문 오류가 있어 파싱을 방해할 때
  • .NET 메서드 호출에서 처리되지 않은 예외가 발생할 때
  • ErrorAction Stop를 명시적으로 지정할 때
  • 치명적인 런타임 오류로 인해 계속 진행할 수 없을 때

비종료 오류

이것들은 스크립트를 중단시키지 않는 보다 일반적인 운영 오류입니다:

  • 파일을 찾을 수 없는 오류
  • 권한 거부 시나리오
  • 네트워크 연결 문제
  • 유효하지 않은 매개변수 값

오류 조치 매개변수: 당신의 첫 번째 방어선

실용적인 예제로 시작해 보겠습니다. 다음은 특정 날짜보다 오래된 파일을 제거하려고 시도하는 스크립트입니다:

param (
    [Parameter(Mandatory)]
    [string]$FolderPath,

    [Parameter(Mandatory)]
    [int]$DaysOld
)

$Now = Get-Date
$LastWrite = $Now.AddDays(-$DaysOld)
$oldFiles = (Get-ChildItem -Path $FolderPath -File -Recurse).Where{$_.LastWriteTime -le $LastWrite}

foreach ($file in $oldFiles) {
    Remove-Item -Path $file.FullName
    Write-Verbose -Message "Successfully removed [$($file.FullName)]."
}

기본적으로 Remove-Item은 비종료 오류를 생성합니다. 이를 종료 오류로 생성하여 우리가 포착할 수 있도록 하려면 -ErrorAction Stop을 추가하세요:

Remove-Item -Path $file.FullName -ErrorAction Stop

Try/Catch 블록: 오류 처리의 다목적 도구

이제 파일 제거를 try/catch 블록으로 감싸봅시다:

foreach ($file in $oldFiles) {
    try {
        Remove-Item -Path $file.FullName -ErrorAction Stop
        Write-Verbose -Message "Successfully removed [$($file.FullName)]."
    }
    catch {
        Write-Warning "Failed to remove file: $($file.FullName)"
        Write-Warning "Error: $($_.Exception.Message)"
    }
}

try 블록에는 오류가 발생할 가능성이 있는 코드가 포함되어 있습니다. 오류가 발생하면 실행은 catch 블록으로 이동합니다(종료 오류인 경우) 여기서 다음을 수행할 수 있습니다:

  • 오류 기록
  • 수정 조치 취하기
  • 관리자에게 알리기
  • 스크립트 실행을 원활하게 계속하기

$Error와 함께 작업하기: 오류 조사 도구

PowerShell은 자동 $Error 변수에 오류 객체의 배열을 유지합니다. $Error를 PowerShell의 “블랙 박스 레코더”라고 생각하세요. 이는 PowerShell 세션 중 발생하는 모든 오류를 기록하여 문제 해결 및 디버깅에 매우 유용합니다.

다음은 $Error를 사용해야 할 때와 이유입니다:

  1. 과거 오류 문제 해결: 빨간 오류 메시지를 놓쳤더라도 $Error는 이력을 유지합니다:

    # 가장 최근 오류 세부정보 보기
    $Error[0] | Format-List * -Force
    
    # 마지막 5개의 오류 보기
    $Error[0..4] | Select-Object CategoryInfo, Exception
    
    # 특정 유형의 오류 검색
    $Error | Where-Object { $_.Exception -is [System.UnauthorizedAccessException] }
    
  2. 스크립트 디버깅: $Error를 사용하여 무엇이 잘못되었고 어디서 발생했는지 이해하세요:

    # 오류가 발생한 정확한 줄 번호와 스크립트 확인
    $Error[0].InvocationInfo | Select-Object ScriptName, ScriptLineNumber, Line
    
    # 전체 오류 호출 스택 보기
    $Error[0].Exception.StackTrace
    
  3. 오류 복구 및 보고: 상세 오류 보고서를 생성하는 데 적합합니다:

    # 오류 보고서 생성
    function Write-ErrorReport {
        param($ErrorRecord = $Error[0])
    [PSCustomObject]@{
        TimeStamp = Get-Date
        ErrorMessage = $ErrorRecord.Exception.Message
        ErrorType = $ErrorRecord.Exception.GetType().Name
        Command = $ErrorRecord.InvocationInfo.MyCommand
        ScriptLine = $ErrorRecord.InvocationInfo.Line
        ErrorLineNumber = $ErrorRecord.InvocationInfo.ScriptLineNumber
        StackTrace = $ErrorRecord.ScriptStackTrace
    }
    

    }

  4. 세션 관리: 오류를 정리하거나 오류 상태를 확인하세요:

    # 오류 기록 지우기 (스크립트 시작 시 유용함)
    $Error.Clear()
    
    # 총 오류 수 세기 (오류 기준 점검에 좋음)
    if ($Error.Count -gt 10) {
        Write-Warning "높은 오류 수가 감지되었습니다: $($Error.Count) 오류"
    }
    

이러한 개념을 결합한 실제 예:

function Test-DatabaseConnections {
    $Error.Clear()  # Start fresh

    try {
        # Attempt database operations...
    }
    catch {
        # If something fails, analyze recent errors
        $dbErrors = $Error | Where-Object {
            $_.Exception.Message -like "*SQL*" -or
            $_.Exception.Message -like "*connection*"
        }

        if ($dbErrors) {
            Write-ErrorReport $dbErrors[0] |
                Export-Csv -Path "C:\\Logs\\DatabaseErrors.csv" -Append
        }
    }
}

전문가 팁:

  • $Error는 PowerShell 세션 당으로 유지됩니다
  • 기본 용량은 256개의 오류로 제한됩니다 ($MaximumErrorCount에 의해 제어됨)
  • 고정 크기 배열입니다 – 가득 차면 새로운 오류가 오래된 것을 밀어냅니다
  • 항상 먼저 $Error[0]를 확인하세요 – 가장 최근의 오류입니다
  • 중요한 스크립트 시작 시 $Error를 지우는 것을 고려하여 깨끗한 오류 추적을 유지

여러 개의 Catch 블록: 목표별 오류 처리

마치 모든 집 수리 작업에 같은 도구를 사용하지 않는 것처럼, PowerShell 오류를 동일한 방식으로 처리해서는 안 됩니다. 여러 catch 블록을 사용하면 다른 유형의 오류에 대해 다르게 응답할 수 있습니다.

작동 방식은 다음과 같습니다:

try {
    Remove-Item -Path $file.FullName -ErrorAction Stop
}
catch [System.UnauthorizedAccessException] {
    # This catches permission-related errors
    Write-Warning "Access denied to file: $($file.FullName)"
    Request-ElevatedPermissions -Path $file.FullName  # Custom function
}
catch [System.IO.IOException] {
    # This catches file-in-use errors
    Write-Warning "File in use: $($file.FullName)"
    Add-ToRetryQueue -Path $file.FullName  # Custom function
}
catch [System.Management.Automation.ItemNotFoundException] {
    # This catches file-not-found errors
    Write-Warning "File not found: $($file.FullName)"
    Update-FileInventory -RemovePath $file.FullName  # Custom function
}
catch {
    # This catches any other errors
    Write-Warning "Unexpected error: $_"
    Write-EventLog -LogName Application -Source "MyScript" -EntryType Error -EventId 1001 -Message $_
}

마주칠 수 있는 일반 오류 유형:

  • [System.UnauthorizedAccessException] – 권한 거부
  • [System.IO.IOException] – 파일 잠금/사용 중
  • [System.Management.Automation.ItemNotFoundException] – 파일/경로를 찾을 수 없음
  • [System.ArgumentException] – 잘못된 인수
  • [System.Net.WebException] – 네트워크/웹 문제

이를 실제로 적용해보는 실제 예제:

function Remove-StaleFiles {
    [CmdletBinding()]
    param(
        [string]$Path,
        [int]$RetryCount = 3,
        [int]$RetryDelaySeconds = 30
    )

    $retryQueue = @()

    foreach ($file in (Get-ChildItem -Path $Path -File)) {
        $attempt = 0
        do {
            $attempt++
            try {
                Remove-Item -Path $file.FullName -ErrorAction Stop
                Write-Verbose "Successfully removed $($file.FullName)"
                break  # Exit the retry loop on success
            }
            catch [System.UnauthorizedAccessException] {
                if ($attempt -eq $RetryCount) {
                    # Log to event log and notify admin
                    $message = "Permission denied after $RetryCount attempts: $($file.FullName)"
                    Write-EventLog -LogName Application -Source "FileCleanup" -EntryType Error -EventId 1001 -Message $message
                    Send-AdminNotification -Message $message  # Custom function
                }
                else {
                    # Request elevated permissions and retry
                    Request-ElevatedAccess -Path $file.FullName  # Custom function
                    Start-Sleep -Seconds $RetryDelaySeconds
                }
            }
            catch [System.IO.IOException] {
                if ($attempt -eq $RetryCount) {
                    # Add to retry queue for later
                    $retryQueue += $file.FullName
                    Write-Warning "File locked, added to retry queue: $($file.FullName)"
                }
                else {
                    # Wait and retry
                    Write-Verbose "File in use, attempt $attempt of $RetryCount"
                    Start-Sleep -Seconds $RetryDelaySeconds
                }
            }
            catch {
                # Unexpected error - log and move on
                $message = "Unexpected error with $($file.FullName): $_"
                Write-EventLog -LogName Application -Source "FileCleanup" -EntryType Error -EventId 1002 -Message $message
                break  # Exit retry loop for unexpected errors
            }
        } while ($attempt -lt $RetryCount)
    }

    # Return retry queue for further processing
    if ($retryQueue) {
        return $retryQueue
    }
}

여러 Catch 블록에 대한 전문가 팁:

  1. 순서가 중요합니다 – 더 구체적인 예외를 먼저 두세요
  2. 각 오류 유형을 일관되게 처리하기 위해 사용자 정의 함수를 사용하세요
  3. 일시적인 오류에 대한 재시도 로직을 고려하세요
  4. 다양한 오류 유형을 서로 다른 위치에 기록하세요
  5. 가능한 한 구체적인 예외 유형을 사용하세요
  6. 각 오류 유형을 의도적으로 발생시켜 각 catch 블록을 테스트하세요

Finally 블록 사용: 스스로 정리하기

finally 블록은 청소 팀과 같아서 오류가 발생하든 발생하지 않든 항상 실행됩니다. 이것은 다음과 같은 작업에 완벽합니다:

  • 파일 핸들 닫기
  • 데이터베이스 연결 끊기
  • 시스템 리소스 해제
  • 원래 설정 복원

실용적인 예는 다음과 같습니다:

try {
    $stream = [System.IO.File]::OpenRead($file.FullName)
    # Process file contents here...
}
catch {
    Write-Warning "Error processing file: $_"
}
finally {
    # This runs even if an error occurred
    if ($stream) {
        $stream.Dispose()
        Write-Verbose "File handle released"
    }
}

finally를 책임감 있는 캠퍼의 규칙처럼 생각하세요: “여행 중 어떤 일이 있었든, 떠나기 전에 항상 캠프장을 정리하세요.”

오류 처리 모범 사례

  1. 오류 작업에 대해 구체적으로 지정하세요
    ErrorAction Stop을 무작위로 사용하는 대신, 오류를 포착해야 하는 명령에서 선택적으로 사용하세요.
  2. 오류 변수를 사용하세요

    Remove-Item $path -ErrorVariable removeError
    if ($removeError) {
        Write-Warning "항목 제거 실패: $($removeError[0].Exception.Message)"
    }
    
  3. 적절하게 오류 기록하기

    • 회복 가능한 오류에는 Write-Warning을 사용하십시오
    • 심각한 문제에는 Write-Error를 사용하십시오
    • 중대한 실패에는 Windows 이벤트 로그에 기록하는 것을 고려하십시오
  4. 리소스 정리
    파일 핸들이나 네트워크 연결과 같은 리소스를 정리하기 위해 반드시 finally 블록을 사용하십시오.

  5. 오류 처리 테스트
    오류를 의도적으로 발생시켜 오류 처리가 예상대로 작동하는지 확인하십시오.

모두 함께 넣기

다음은 이러한 모범 사례를 포함한 완전한 예제입니다:

function Remove-OldFiles {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$FolderPath,

        [Parameter(Mandatory)]
        [int]$DaysOld,

        [string]$LogPath = "C:\\Logs\\file-cleanup.log"
    )

    try {
        # Validate input
        if (-not (Test-Path -Path $FolderPath)) {
            throw "Folder path '$FolderPath' does not exist"
        }

        $Now = Get-Date
        $LastWrite = $Now.AddDays(-$DaysOld)

        # Find old files
        $oldFiles = Get-ChildItem -Path $FolderPath -File -Recurse |
                    Where-Object {$_.LastWriteTime -le $LastWrite}

        foreach ($file in $oldFiles) {
            try {
                Remove-Item -Path $file.FullName -ErrorAction Stop
                Write-Verbose -Message "Successfully removed [$($file.FullName)]"

                # Log success
                "$(Get-Date) - Removed file: $($file.FullName)" |
                    Add-Content -Path $LogPath
            }
            catch [System.UnauthorizedAccessException] {
                Write-Warning "Access denied to file: $($file.FullName)"
                "$ErrorActionPreference - Access denied: $($file.FullName)" |
                    Add-Content -Path $LogPath
            }
            catch [System.IO.IOException] {
                Write-Warning "File in use: $($file.FullName)"
                "$(Get-Date) - File in use: $($file.FullName)" |
                    Add-Content -Path $LogPath
            }
            catch {
                Write-Warning "Unexpected error removing file: $_"
                "$(Get-Date) - Error: $_ - File: $($file.FullName)" |
                    Add-Content -Path $LogPath
            }
        }
    }
    catch {
        Write-Error "Critical error in Remove-OldFiles: $_"
        "$(Get-Date) - Critical Error: $_" |
            Add-Content -Path $LogPath
        throw  # Re-throw error to calling script
    }
}

이 구현은 다음을 제공합니다:

  • 입력 매개변수를 유효성 검사합니다
  • 일반 오류에 대해 특정 catch 블록을 사용합니다
  • 성공과 실패 모두 기록합니다
  • 문제 해결을 위한 자세한 출력을 제공합니다
  • 중대한 오류는 호출 스크립트로 다시 throw합니다

결론

적절한 오류 처리는 신뢰할 수 있는 PowerShell 스크립트에 중요합니다. 오류 유형을 이해하고 try/catch 블록을 효과적으로 사용함으로써, 실패를 우아하게 처리하고 의미 있는 피드백을 제공하는 스크립트를 작성할 수 있습니다. 오류 처리를 철저히 테스트하는 것을 기억하세요 – 제품에서 문제를 해결할 때 미래의 자신이 감사할 것입니다!

자, 그 오류들을 잡아보세요! 기억하세요 – 처리되지 않은 오류만이 나쁜 오류입니다.

Source:
https://adamtheautomator.com/powershell-error-handling/