Post

[PowerShell] CmdletBinding과 자동 변수로 고급 함수 만들기

PowerShell의 CmdletBinding 어노테이션과 자동 변수를 활용한 프로페셔널한 스크립트 작성법

CmdletBinding이란?

[CmdletBinding()]은 PowerShell 함수를 고급 함수(Advanced Function)로 만들어주는 어노테이션이다. Java의 @annotation과 유사하게, 함수에 추가 기능을 부여한다.

기본 함수 vs 고급 함수

1
2
3
4
5
6
7
8
9
10
11
12
# 기본 함수 (Simple Function)
function Get-SimpleData {
    param([string]$Name)
    Write-Host "Name: $Name"
}

# 고급 함수 (Advanced Function)
function Get-AdvancedData {
    [CmdletBinding()]  # 👈 이것만 추가하면 고급 함수!
    param([string]$Name)
    Write-Host "Name: $Name"
}

차이점

기능기본 함수고급 함수 (CmdletBinding)
-Verbose
-Debug
-ErrorAction
-WarningAction
-InformationAction
$PSCmdlet 변수
Pipeline 지원제한적완전 지원

실전 예제: 보안 스캐너

Windows Agent의 보안 스캔 스크립트에서 CmdletBinding을 활용한 실제 코드를 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<#
.SYNOPSIS
ITSM Agent - Security Scan Tool

.DESCRIPTION
수동으로 보안 스캔을 수행하는 도구입니다.

.PARAMETER ConfigPath
설정 파일 경로 (기본값: 현재 디렉토리의 config\agent-config.json)

.PARAMETER OutputFile
스캔 결과를 저장할 파일 경로 (JSON 형식, 선택사항)

.PARAMETER SendToBackend
스캔 결과를 Backend-portal로 전송합니다

.EXAMPLE
.\Invoke-SecurityScan.ps1

.EXAMPLE
.\Invoke-SecurityScan.ps1 -OutputFile "C:\temp\scan-results.json"

.EXAMPLE
.\Invoke-SecurityScan.ps1 -SendToBackend

.EXAMPLE
.\Invoke-SecurityScan.ps1 -OutputFile "C:\temp\scan-results.json" -SendToBackend
#>

[CmdletBinding()]  # 👈 고급 함수 선언
param(
    [string]$ConfigPath = "$PSScriptRoot\config\agent-config.json",
    [string]$OutputFile = "",
    [switch]$SendToBackend  # 👈 스위치 파라미터 (true/false)
)

CmdletBinding의 이점

  1. 자동으로 -Verbose 지원 ```powershell

    실행 시

    .\Invoke-SecurityScan.ps1 -Verbose

스크립트 내부에서

Write-Verbose “Loading security patterns…” # -Verbose 시에만 출력

1
2
3
4
5
6
2. **에러 처리 개선**
```powershell
# -ErrorAction 자동 지원
.\Invoke-SecurityScan.ps1 -ErrorAction Stop
.\Invoke-SecurityScan.ps1 -ErrorAction SilentlyContinue
  1. 파이프라인 처리 ```powershell [CmdletBinding()] param( [Parameter(ValueFromPipeline=$true)] [string]$InputObject )

process { # 파이프라인으로 들어오는 각 항목 처리 Write-Host “Processing: $InputObject” }

사용 예

Get-Content files.txt | .\Invoke-SecurityScan.ps1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
---

## PowerShell 자동 변수 (Automatic Variables)

PowerShell은 스크립트 실행 시 자동으로 생성되는 변수들을 제공한다.

### 주요 자동 변수

| 변수 | 설명 | 예시 |
|------|------|------|
| `$PSScriptRoot` | 스크립트가 있는 디렉토리 경로 | `C:\Scripts` |
| `$PSCommandPath` | 실행 중인 스크립트 전체 경로 | `C:\Scripts\scan.ps1` |
| `$MyInvocation` | 현재 명령의 실행 정보 | `$MyInvocation.MyCommand.Name` |
| `$PSBoundParameters` | 전달된 파라미터 목록 | `@{ConfigPath="..."}` |
| `$PSCmdlet` | Cmdlet 관련 메서드 제공 | `$PSCmdlet.ShouldProcess()` |
| `$Error` | 에러 히스토리 배열 | `$Error[0]` (최근 에러) |
| `$?` | 마지막 명령 성공 여부 | `$true` 또는 `$false` |
| `$null` | Null 값 | `if ($var -eq $null)` |

### 실전 활용: $PSScriptRoot

```powershell
# ❌ 나쁜 예: 하드코딩된 경로
$ConfigPath = "C:\Program Files\ITSM\config\agent-config.json"

# ✅ 좋은 예: 상대 경로 사용
$ConfigPath = "$PSScriptRoot\config\agent-config.json"

# 모듈 임포트도 동일하게
$ModulesDir = Join-Path $PSScriptRoot "modules"
Import-Module "$ModulesDir\Logger.psm1" -Force
Import-Module "$ModulesDir\Security.psm1" -Force

출력 버리기 (Discarding Output)

PowerShell에서는 모든 출력이 파이프라인으로 전달된다. 불필요한 출력을 버리는 여러 방법이 있다.

방법 1: $null에 할당

1
2
3
4
# 👍 가장 빠른 방법
$null = cmd /c chcp 65001 2>&1

# 설명: cmd 출력을 $null 변수에 할당 → 버려짐

방법 2: Out-Null

1
2
# 👎 느림 (파이프라인 오버헤드)
New-Item -ItemType Directory -Path $LogDir -Force | Out-Null

방법 3: [void]

1
2
# 👍 빠름
[void](New-Item -ItemType Directory -Path $LogDir -Force)

방법 4: > $null

1
2
# 👎 파일 리다이렉션 오버헤드
cmd /c chcp 65001 > $null 2>&1

성능 비교

방법속도권장
$null = ...가장 빠름✅ 권장
[void](...)빠름✅ 권장
... > $null보통⚠️ 파일 작업 시 사용
... | Out-Null느림❌ 비권장

해시테이블 vs 스크립트 블록

PowerShell에서 중괄호 {}는 컨텍스트에 따라 다르게 해석된다.

해시테이블 @{}

1
2
3
4
5
6
7
8
9
10
11
12
# @ 기호로 해시테이블 선언
$agentMetadata = @{
    agent_id = $Config.agent.id
    agent_version = $Config.agent.version
    group_name = $Config.agent.group_name
    os_type = "Windows"
    os_version = (Get-WmiObject Win32_OperatingSystem).Caption
}

# 접근
Write-Host $agentMetadata.agent_id
Write-Host $agentMetadata["os_type"]

스크립트 블록 {}

1
2
3
4
5
6
7
8
# @ 없이 중괄호만 사용 → 실행 가능한 코드 블록
$scriptBlock = {
    Write-Host "This is executable code"
    Get-Process | Where-Object CPU -gt 10
}

# 실행
& $scriptBlock  # 또는 Invoke-Command -ScriptBlock $scriptBlock

실전 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 해시테이블: 데이터 구조
$scanReport = @{
    agent_id = $Config.agent.id
    scan_timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
    total_findings = $findings.Count
    summary = @{
        by_severity = $severityCount
        by_type = $typeCount
    }
    findings = $findingsData
}

# JSON 변환
$jsonData = $scanReport | ConvertTo-Json -Depth 10

타입 지정 (Type Constraints)

PowerShell은 동적 타입이지만, 명시적 타입 지정이 가능하다.

기본 타입

1
2
3
4
5
6
7
8
9
# 파라미터 타입 지정
[CmdletBinding()]
param(
    [string]$ConfigPath,      # 문자열
    [int]$MaxSize,            # 정수
    [bool]$Enabled,           # 불린
    [switch]$SendToBackend,   # 스위치 (특별한 불린)
    [object]$Data             # 모든 타입 허용 (root type)
)

배열과 컬렉션

1
2
3
4
5
6
7
8
9
10
11
12
# 배열 타입
[string[]]$Directories = @("/home", "/var/www", "/tmp")

# 해시테이블 타입
[hashtable]$Config = @{
    enabled = $true
    max_size_mb = 10
}

# 제네릭 리스트
[System.Collections.Generic.List[string]]$Findings = @()
$Findings.Add("finding1")

리턴 타입

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Get-SecurityPatterns {
    [CmdletBinding()]
    param([object]$Config)

    # 리턴 타입은 명시하지 않지만, 문서화 블록에 명시
    # .OUTPUTS System.Array

    return @(
        @{ id = "ssn-us"; type = "PII" },
        @{ id = "credit-card"; type = "PCI" }
    )
}

# 리턴 타입 명시 (잘 안 쓰임)
[Array] Get-SecurityPatterns {
    param([object]$Config)
    return @()
}

인코딩 설정 (UTF-8 전환)

PowerShell의 기본 인코딩은 시스템 로케일을 따르는데, 한글 Windows는 CP949(Windows-1252)를 사용한다. UTF-8로 전환하는 표준 패턴이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Console encoding settings
try {
    # 1. 콘솔 코드 페이지를 UTF-8(65001)로 변경
    $null = cmd /c chcp 65001 2>&1

    # 2. 출력 인코딩 설정
    $OutputEncoding = [System.Text.Encoding]::UTF8

    # 3. 콘솔 입출력 인코딩 설정
    [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
    [Console]::InputEncoding = [System.Text.Encoding]::UTF8

    # 4. 모든 파일 관련 cmdlet의 기본 인코딩을 UTF-8로 설정
    $PSDefaultParameterValues['*:Encoding'] = 'utf8'
    $PSDefaultParameterValues['Out-File:Encoding'] = 'utf8'
    $PSDefaultParameterValues['Set-Content:Encoding'] = 'utf8'
    $PSDefaultParameterValues['Add-Content:Encoding'] = 'utf8'
} catch {
    # 인코딩 설정 실패 시 무시 (일부 환경에서 권한 문제 발생 가능)
}

코드 페이지 (Code Page)

코드 페이지인코딩설명
949CP949한글 Windows 기본 (EUC-KR 확장)
1252Windows-1252영문 Windows 기본
65001UTF-8유니코드, 전 세계 문자 지원
437OEM-USDOS 기본 코드 페이지

에러 처리 패턴

1
2
3
4
5
6
7
8
9
$ErrorActionPreference = "Stop"  # 모든 에러를 예외로 처리

try {
    $Config = Get-Content -Path $ConfigPath -Raw | ConvertFrom-Json
}
catch {
    Write-Host "Failed to load configuration: $_" -ForegroundColor Red
    exit 1
}

에러 정보 접근

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
try {
    Invoke-RestMethod -Uri $url -Method GET
}
catch [System.Net.WebException] {
    # 특정 예외 타입 처리
    if ($_.Exception.Response) {
        $statusCode = $_.Exception.Response.StatusCode.value__
        $statusDescription = $_.Exception.Response.StatusDescription
        Write-Host "HTTP Error: $statusCode $statusDescription"
    }
}
catch {
    # 모든 예외 처리
    $errorMessage = $_.Exception.Message
    $exceptionType = $_.Exception.GetType().FullName
    Write-Host "Error: $errorMessage"
    Write-Host "Type: $exceptionType"
}

실전 팁 (Best Practices)

1. 항상 CmdletBinding 사용

1
2
3
4
5
6
# ✅ 좋은 예
[CmdletBinding()]
param([string]$Path)

# ❌ 나쁜 예
param([string]$Path)

2. 상대 경로는 $PSScriptRoot

1
2
3
4
5
# ✅ 좋은 예
$ConfigPath = "$PSScriptRoot\config\settings.json"

# ❌ 나쁜 예
$ConfigPath = ".\config\settings.json"  # 현재 작업 디렉토리 기준 (위험)

3. 출력 버리기는 $null 할당

1
2
3
4
5
# ✅ 좋은 예 (빠름)
$null = New-Item -Path $dir -Force

# ❌ 나쁜 예 (느림)
New-Item -Path $dir -Force | Out-Null

4. 타입은 명시적으로

1
2
3
4
5
6
7
8
9
10
# ✅ 좋은 예
[CmdletBinding()]
param(
    [string]$Name,
    [int]$Age,
    [switch]$Force
)

# ❌ 나쁜 예
param($Name, $Age, $Force)

마치며

PowerShell의 고급 기능을 활용하면 더 강력하고 유지보수하기 쉬운 스크립트를 작성할 수 있습니다.

CmdletBinding을 고급 함수의 기본으로 항상 사용하고, $PSScriptRoot, $PSCmdlet 등 자동 변수를 적극 활용합니다. 출력 버리기는 $null = ... 패턴을 사용하고, 해시테이블(@{})과 실행 블록({})을 구분합니다. 파라미터 타입을 명시하여 안정성을 확보하고, UTF-8 전환 패턴을 표준화합니다.

Java의 어노테이션, Go의 구조체와 마찬가지로, PowerShell도 언어의 특성을 이해하고 활용하면 훨씬 프로페셔널한 코드를 작성할 수 있습니다.

도움이 되셨길 바랍니다! 😀

This post is licensed under CC BY 4.0 by the author.