PowerShell 클래스 메서드

PowerShell 클래스 메서드 (about_Classes_Methods)

PowerShell 클래스 안에서 메서드를 정의하는 방법을 설명해 드릴게요. 메서드는 클래스가 수행할 수 있는 동작을 정의해요. 여러분이 함수에 익숙하시다면, 클래스 메서드는 함수와 비슷하면서도 결과가 파이프라인으로 새어 나가지 않는다는 점이 크게 다르답니다.

출처: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_classes_methods

본문

간단한 설명

PowerShell 클래스에 메서드를 정의하는 방법을 설명해요.

자세한 설명

메서드는 클래스가 수행할 수 있는 동작을 정의해요. 메서드는 입력 데이터를 지정하는 매개 변수를 받을 수 있고, 항상 출력 형식을 정의해요. 메서드가 어떤 출력도 돌려주지 않는다면 Void 출력 형식을 가져야 하고, 출력 형식을 따로 지정하지 않아도 기본 출력 형식은 Void예요.

클래스 메서드 안에서는 return 문에 명시된 객체만 파이프라인으로 전달돼요. 코드 어디선가 의도치 않게 값이 파이프라인으로 흘러나가는 일이 없어요.

참고 이건 PowerShell 함수가 출력을 처리하는 방식과 근본적으로 달라요. 함수에서는 모든 게 파이프라인으로 넘어가거든요.

클래스 메서드 안에서 오류 스트림으로 쓴 종료 오류(nonterminating error)는 밖으로 전달되지 않아요. 종료 오류를 밖으로 보내려면 throw를 써야 해요.

Write-* cmdlet을 쓰면 클래스 메서드 안에서도 PowerShell의 출력 스트림에 쓸 수 있는데, 이 cmdlet들은 호출하는 쪽의 preference variables를 따르게 돼요. 다만 메서드가 return 문으로만 객체를 출력하도록 하고 싶다면 Write-* cmdlet 사용은 피하는 게 좋아요.

클래스 메서드는 $this 자동 변수로 현재 클래스 인스턴스를 참조해서 현재 클래스에 정의된 속성과 다른 메서드에 접근할 수 있어요. 단, $this 자동 변수는 정적 메서드에서는 쓸 수 없어요.

클래스 메서드에는 hidden이나 static 등 원하는 만큼 특성(attribute)을 붙일 수 있어요.

구문

클래스 메서드는 다음 구문을 써요.

한 줄 구문

[[<attribute>]...] [hidden] [static] [<output-type>] <method-name> ([<method-parameters>]) { <body> }

여러 줄 구문

[[<attribute>]...]
[hidden]
[static]
[<output-type>] <method-name> ([<method-parameters>]) {
  <body>
}

예제

예제 1 - 가장 간단한 메서드 정의

ExampleCube1 클래스의 GetVolume() 메서드는 큐브의 부피를 돌려줘요. 출력 형식을 실수로 정하고, 인스턴스의 Height, Length, Width 속성을 곱한 결과를 반환해요.

class ExampleCube1 {
    [float]   $Height
    [float]   $Length
    [float]   $Width

    [float] GetVolume() { return $this.Height * $this.Length * $this.Width }
}

$box = [ExampleCube1]@{
    Height = 2
    Length = 2
    Width  = 3
}

$box.GetVolume()
12

예제 2 - 매개 변수가 있는 메서드

GeWeight() 메서드는 큐브의 밀도를 실수로 입력받아, 부피에 밀도를 곱해 계산한 큐브의 무게를 반환해요.

class ExampleCube2 {
    [float]   $Height
    [float]   $Length
    [float]   $Width

    [float] GetVolume() { return $this.Height * $this.Length * $this.Width }
    [float] GetWeight([float]$Density) {
        return $this.GetVolume() * $Density
    }
}

$cube = [ExampleCube2]@{
    Height = 2
    Length = 2
    Width  = 3
}

$cube.GetWeight(2.5)
30

예제 3 - 출력이 없는 메서드

이 예제는 출력 형식을 System.Void로 하는 Validate() 메서드를 정의해요. 이 메서드는 어떤 출력도 반환하지 않고, 대신 검증에 실패하면 오류를 던져요. GetVolume() 메서드는 큐브의 부피를 계산하기 전에 Validate()를 호출해요. 검증에 실패하면 계산 전에 메서드가 끝나 버려요.

class ExampleCube3 {
    [float]   $Height
    [float]   $Length
    [float]   $Width

    [float] GetVolume() {
        $this.Validate()

        return $this.Height * $this.Length * $this.Width
    }

    [void] Validate() {
        $InvalidProperties = @()
        foreach ($Property in @('Height', 'Length', 'Width')) {
            if ($this.$Property -le 0) {
                $InvalidProperties += $Property
            }
        }

        if ($InvalidProperties.Count -gt 0) {
            $Message = @(
                'Invalid cube properties'
                "('$($InvalidProperties -join "', '")'):"
                "Cube dimensions must all be positive numbers."
            ) -join ' '
            throw $Message
        }
    }
}

$Cube = [ExampleCube3]@{ Length = 1 ; Width = -1 }
$Cube

$Cube.GetVolume()
Height Length Width
------ ------ -----
  0.00   1.00 -1.00

Exception:
Line |
  20 |              throw $Message
     |              ~~~~~~~~~~~~~~
     | Invalid cube properties ('Height', 'Width'): Cube dimensions must
     | all be positive numbers.

HeightWidth 속성이 올바르지 않아 메서드가 예외를 던지면서, 클래스가 현재 큐브의 부피를 계산하지 못하게 돼요.

예제 4 - 오버로드가 있는 정적 메서드

ExampleCube4 클래스는 두 개의 오버로드를 가진 정적 메서드 GetVolume()을 정의해요. 첫 번째 오버로드는 큐브의 각 차원과, 입력을 검증할지 여부를 나타내는 플래그를 매개 변수로 받아요.

두 번째 오버로드는 숫자 입력만 받고, 첫 번째 오버로드를 $Strict$true로 해서 호출해요. 다시 말해 두 번째 오버로드를 쓰면 꼭 입력을 엄격히 검증할지 정하지 않아도 되도록, 사용자에게 좀 더 간편한 방법을 주는 거예요.

클래스는 또한 GetVolume()을 인스턴스(비정적) 메서드로 정의해요. 이 메서드는 두 번째 정적 오버로드를 호출해서, 인스턴스 GetVolume() 메서드가 결과를 반환하기 전에 항상 큐브의 각 차원을 검증하도록 해요.

class ExampleCube4 {
    [float]   $Height
    [float]   $Length
    [float]   $Width

    static [float] GetVolume(
        [float]$Height,
        [float]$Length,
        [float]$Width,
        [boolean]$Strict
    ) {
        $Signature = "[ExampleCube4]::GetVolume({0}, {1}, {2}, {3})"
        $Signature = $Signature -f $Height, $Length, $Width, $Strict
        Write-Verbose "Called $Signature"

        if ($Strict) {
            [ValidateScript({$_ -gt 0 })]$Height = $Height
            [ValidateScript({$_ -gt 0 })]$Length = $Length
            [ValidateScript({$_ -gt 0 })]$Width  = $Width
        }

        return $Height * $Length * $Width
    }

    static [float] GetVolume([float]$Height, [float]$Length, [float]$Width) {
        $Signature = "[ExampleCube4]::GetVolume($Height, $Length, $Width)"
        Write-Verbose "Called $Signature"

        return [ExampleCube4]::GetVolume($Height, $Length, $Width, $true)
    }

    [float] GetVolume() {
        Write-Verbose "Called `$this.GetVolume()"
        return [ExampleCube4]::GetVolume(
            $this.Height,
            $this.Length,
            $this.Width
        )
    }
}

$VerbosePreference = 'Continue'
$Cube = [ExampleCube4]@{ Height = 2 ; Length = 2 }
$Cube.GetVolume()
VERBOSE: Called $this.GetVolume()
VERBOSE: Called [ExampleCube4]::GetVolume(2, 2, 0)
VERBOSE: Called [ExampleCube4]::GetVolume(2, 2, 0, True)

MetadataError:
Line |
  19 |              [ValidateScript({$_ -gt 0 })]$Width  = $Width
     |              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | The variable cannot be validated because the value 0 is not a valid
     | value for the Width variable.

메서드 정의 안의 자세한 메시지들이 $this.GetVolume() 호출이 어떻게 정적 메서드를 호출하는지 보여줘요.

Strict 매개 변수를 $false로 해서 정적 메서드를 직접 호출하면 부피로 0을 반환해요.

[ExampleCube4]::GetVolume($Cube.Height, $Cube.Length, $Cube.Width, $false)
VERBOSE: Called [ExampleCube4]::GetVolume(2, 2, 0, False)
0

메서드 시그니처와 오버로드

모든 클래스 메서드에는 메서드를 어떻게 호출할지 결정하는 고유한 시그니처가 있어요. 메서드의 출력 형식, 이름, 매개 변수가 메서드 시그니처를 정의해요.

한 클래스에 같은 이름의 메서드가 둘 이상 정의되면 그 메서드 정의들을 오버로드(overload) 라고 불러요. 메서드의 오버로드들은 매개 변수가 서로 달라야 해요. 출력 형식이 다르더라도 매개 변수가 같은 두 구현을 한 메서드에 정의할 수는 없어요.

다음 클래스는 Shuffle()Deal() 두 메서드를 정의해요. Deal() 메서드는 매개 변수가 없는 것과 Count 매개 변수가 있는 것, 두 가지 오버로드를 정의해요.

class CardDeck {
    [string[]]$Cards  = @()
    hidden [string[]]$Dealt  = @()
    hidden [string[]]$Suits  = @('Clubs', 'Diamonds', 'Hearts', 'Spades')
    hidden [string[]]$Values = 2..10 + @('Jack', 'Queen', 'King', 'Ace')

    CardDeck() {
        foreach($Suit in $this.Suits) {
            foreach($Value in $this.Values) {
                $this.Cards += "$Value of $Suit"
            }
        }
        $this.Shuffle()
    }

    [void] Shuffle() {
        $this.Cards = $this.Cards + $this.Dealt | Where-Object -FilterScript {
             -not [string]::IsNullOrEmpty($_)
        } | Get-Random -Count $this.Cards.Count
    }

    [string] Deal() {
        if ($this.Cards.Count -eq 0) { throw "There are no cards left." }

        $Card        = $this.Cards[0]
        $this.Cards  = $this.Cards[1..$this.Cards.Count]
        $this.Dealt += $Card

        return $Card
    }

    [string[]] Deal([int]$Count) {
        if ($Count -gt $this.Cards.Count) {
            throw "There are only $($this.Cards.Count) cards left."
        } elseif ($Count -lt 1) {
            throw "You must deal at least 1 card."
        }

        return (1..$Count | ForEach-Object { $this.Deal() })
    }
}

메서드 출력

기본적으로 메서드는 어떤 출력도 가지지 않아요. 메서드 시그니처에 Void가 아닌 명시적 출력 형식이 포함되면, 그 메서드는 해당 형식의 객체를 반환해야 해요. return 키워드가 명시적으로 객체를 반환할 때만 메서드가 출력을 내보내요.

메서드 매개 변수

클래스 메서드는 메서드 본문에서 사용할 입력 매개 변수를 정의할 수 있어요. 메서드 매개 변수는 괄호 안에 넣고 쉼표로 구분해요. 괄호가 비어 있으면 해당 메서드에 매개 변수가 필요 없다는 뜻이에요.

매개 변수는 한 줄 또는 여러 줄로 정의할 수 있어요. 다음 블록들이 메서드 매개 변수의 구문을 보여줘요.

([[<parameter-type>]]$<parameter-name>[, [[<parameter-type>]]$<parameter-name>])
(
    [[<parameter-type>]]$<parameter-name>[,
    [[<parameter-type>]]$<parameter-name>]
)

메서드 매개 변수는 강한 형식(strongly typed)으로 지정할 수 있어요. 형식을 지정하지 않으면 해당 매개 변수는 어떤 객체든 받아들이고, 형식을 지정하면 메서드가 그 매개 변수의 값을 올바른 형식으로 변환하려 시도해요. 변환할 수 없으면 예외를 던져요.

메서드 매개 변수는 기본값을 정의할 수 없고, 모든 메서드 매개 변수는 필수예요.

메서드 매개 변수에는 다른 어떤 특성도 붙일 수 없어요. 그래서 메서드가 Validate* 특성을 가진 매개 변수를 쓰지 못해요. 검증 특성에 대한 자세한 내용은 about_Functions_Advanced_Parameters 문서를 봐 주세요.

메서드 매개 변수에 검증을 추가하려면 다음 패턴 중 하나를 쓸 수 있어요.

  • 매개 변수를 필요한 검증 특성과 함께 같은 변수에 다시 할당하는 방법. 정적 메서드와 인스턴스 메서드 둘 다에서 동작해요. 이 패턴의 예시는 Example 4를 봐 주세요.
  • Update-TypeData로 검증 특성을 매개 변수에 직접 넣은 ScriptMethod를 정의하는 방법. 이 방법은 인스턴스 메서드에서만 동작해요. 자세한 내용은 Defining instance methods with Update-TypeData 부분을 봐 주세요.

메서드에서의 자동 변수

모든 자동 변수를 메서드에서 쓸 수 있는 건 아니에요. 다음 목록은 자동 변수들과, PowerShell 클래스 메서드에서 이들을 쓰는 게 좋은지/어떻게 쓰는지에 대한 권장 사항이에요. 목록에 없는 자동 변수는 클래스 메서드에서 사용할 수 없어요.

  • $? - 평소처럼 접근해요.
  • $_ - 평소처럼 접근해요.
  • $args - 대신 명시적 매개 변수 변수를 써요.
  • $ConsoleFileName - 대신 $Script:ConsoleFileName으로 접근해요.
  • $Error - 평소처럼 접근해요.
  • $EnabledExperimentalFeatures - 대신 $Script:EnabledExperimentalFeatures로 접근해요.
  • $Event - 평소처럼 접근해요.
  • $EventArgs - 평소처럼 접근해요.
  • $EventSubscriber - 평소처럼 접근해요.
  • $ExecutionContext - 대신 $Script:ExecutionContext로 접근해요.
  • $false - 평소처럼 접근해요.
  • $foreach - 평소처럼 접근해요.
  • $HOME - 대신 $Script:HOME으로 접근해요.
  • $Host - 대신 $Script:Host로 접근해요.
  • $input - 대신 명시적 매개 변수 변수를 써요.
  • $IsCoreCLR - 대신 $Script:IsCoreCLR로 접근해요.
  • $IsLinux - 대신 $Script:IsLinux로 접근해요.
  • $IsMacOS - 대신 $Script:IsMacOS로 접근해요.
  • $IsWindows - 대신 $Script:IsWindows로 접근해요.
  • $LASTEXITCODE - 평소처럼 접근해요.
  • $Matches - 평소처럼 접근해요.
  • $MyInvocation - 평소처럼 접근해요.
  • $NestedPromptLevel - 평소처럼 접근해요.
  • $null - 평소처럼 접근해요.
  • $PID - 대신 $Script:PID로 접근해요.
  • $PROFILE - 대신 $Script:PROFILE로 접근해요.
  • $PSBoundParameters - 이 변수는 쓰지 마세요. cmdlet과 함수를 위해 만들어진 거라 클래스에서 쓰면 예상 밖의 부작용이 생길 수 있어요.
  • $PSCmdlet - 이 변수는 쓰지 마세요. cmdlet과 함수를 위해 만들어진 거라 클래스에서 쓰면 예상 밖의 부작용이 생길 수 있어요.
  • $PSCommandPath - 평소처럼 접근해요.
  • $PSCulture - 대신 $Script:PSCulture로 접근해요.
  • $PSEdition - 대신 $Script:PSEdition으로 접근해요.
  • $PSHOME - 대신 $Script:PSHOME으로 접근해요.
  • $PSItem - 평소처럼 접근해요.
  • $PSScriptRoot - 평소처럼 접근해요.
  • $PSSenderInfo - 대신 $Script:PSSenderInfo로 접근해요.
  • $PSUICulture - 대신 $Script:PSUICulture로 접근해요.
  • $PSVersionTable - 대신 $Script:PSVersionTable로 접근해요.
  • $PWD - 평소처럼 접근해요.
  • $Sender - 평소처럼 접근해요.
  • $ShellId - 대신 $Script:ShellId로 접근해요.
  • $StackTrace - 평소처럼 접근해요.
  • $switch - 평소처럼 접근해요.
  • $this - 평소처럼 접근해요. 클래스 메서드에서 $this는 항상 클래스의 현재 인스턴스예요. 이를 통해 클래스 속성과 메서드에 접근할 수 있고, 정적 메서드에서는 사용할 수 없어요.
  • $true - 평소처럼 접근해요.

자동 변수에 대한 자세한 내용은 about_Automatic_Variables 문서를 봐 주세요.

숨겨진 메서드 (Hidden methods)

hidden 키워드로 선언하면 클래스의 메서드를 숨길 수 있어요. 숨겨진 클래스 메서드는 다음과 같은 특징이 있어요.

  • Get-Member cmdlet이 반환하는 클래스 멤버 목록에 포함되지 않아요. Get-Member로 숨겨진 메서드를 보려면 Force 매개 변수를 써요.
  • 숨겨진 메서드를 정의한 클래스 안에서 완성(completion)이 일어나는 경우가 아니라면, 탭 완성이나 IntelliSense에 표시되지 않아요.
  • 클래스의 공용 멤버예요. 호출하고 상속할 수 있죠. 메서드를 숨겨도 private이 되는 건 아니에요. 위에서 설명한 대로 목록에서 제외되는 것뿐이에요.

참고 메서드의 오버로드 중 하나라도 숨기면, 그 메서드는 IntelliSense, 완성 결과, Get-Member의 기본 출력에서 모두 제거돼요.

hidden 키워드에 대한 자세한 내용은 about_Hidden 문서를 봐 주세요.

정적 메서드 (Static methods)

static 키워드로 선언하면 메서드를 클래스 인스턴스가 아니라 클래스 자체에 속하는 것으로 정의할 수 있어요. 정적 클래스 메서드는 다음과 같은 특징이 있어요.

  • 클래스 인스턴스화와 무관하게 항상 사용할 수 있어요.
  • 클래스의 모든 인스턴스 간에 공유돼요.
  • 항상 사용 가능해요.
  • 클래스의 인스턴스 속성에는 접근할 수 없고, 정적 속성에만 접근할 수 있어요.
  • 세션 전체 동안 살아 있어요.

파생 클래스 메서드

클래스가 기본 클래스에서 파생되면, 기본 클래스의 메서드와 그 오버로드를 상속받아요. 숨겨진 메서드를 포함해 기본 클래스에 정의된 모든 메서드 오버로드를 파생 클래스에서 사용할 수 있어요.

파생 클래스는 클래스 정의에서 상속받은 메서드 오버로드를 다시 정의해 재정의(override)할 수 있어요. 오버로드를 재정의하려면 매개 변수 형식이 기본 클래스와 같아야 해요. 오버로드의 출력 형식은 달라도 돼요.

생성자와 달리 메서드는 : base(<parameters>) 구문으로 기본 클래스의 메서드 오버로드를 호출할 수 없어요. 파생 클래스에서 재정의한 오버로드가 기본 클래스가 정의한 오버로드를 완전히 대체해요.

다음 예제는 파생 클래스에서 정적 메서드와 인스턴스 메서드가 어떻게 동작하는지 보여줘요.

기본 클래스가 정의하는 것들:

  • 현재 시간을 반환하는 정적 메서드 Now()와 과거의 날짜를 반환하는 DaysAgo().
  • 인스턴스 속성 TimeStamp와, 그 속성의 문자열 표현을 반환하는 인스턴스 메서드 ToString(). 덕분에 인스턴스가 문자열에서 쓰일 때 클래스 이름 대신 datetime 문자열로 변환돼요.
  • 두 개의 오버로드를 가진 인스턴스 메서드 SetTimeStamp(). 매개 변수 없이 호출하면 TimeStamp를 현재 시간으로 설정하고, DateTime을 받아서 호출하면 이를 그 값으로 설정해요.
class BaseClass {
    static [datetime] Now() {
        return Get-Date
    }
    static [datetime] DaysAgo([int]$Count) {
        return [BaseClass]::Now().AddDays(-$Count)
    }

    [datetime] $TimeStamp = [BaseClass]::Now()

    [string] ToString() {
        return $this.TimeStamp.ToString()
    }

    [void] SetTimeStamp([datetime]$TimeStamp) {
        $this.TimeStamp = $TimeStamp
    }
    [void] SetTimeStamp() {
        $this.TimeStamp = [BaseClass]::Now()
    }
}

다음 블록은 BaseClass에서 파생된 클래스들을 정의해요.

  • DerivedClassA는 어떤 재정의도 없이 BaseClass를 상속해요.
  • DerivedClassB는 정적 메서드 DaysAgo()를 재정의해 DateTime 객체 대신 문자열 표현을 반환하고, 인스턴스 메서드 ToString()도 재정의해 타임스탬프를 ISO8601 날짜 문자열로 반환해요.
  • DerivedClassC는 매개 변수가 없는 SetTimeStamp() 오버로드를 재정의해서, 매개 변수 없이 타임스탬프를 설정하면 현재 날짜보다 10일 전 날짜로 설정하게 해요.
class DerivedClassA : BaseClass     {}
class DerivedClassB : BaseClass     {
    static [string] DaysAgo([int]$Count) {
        return [BaseClass]::DaysAgo($Count).ToString('yyyy-MM-dd')
    }
    [string] ToString() {
        return $this.TimeStamp.ToString('yyyy-MM-dd')
    }
}
class DerivedClassC : BaseClass {
    [void] SetTimeStamp() {
        $this.SetTimeStamp([BaseClass]::Now().AddDays(-10))
    }
}

다음 블록은 정의된 클래스들의 정적 Now() 메서드 출력을 보여줘요. 파생 클래스들이 메서드의 기본 클래스 구현을 재정의하지 않았기 때문에 모든 클래스의 출력이 같아요.

"[BaseClass]::Now()     => $([BaseClass]::Now())"
"[DerivedClassA]::Now() => $([DerivedClassA]::Now())"
"[DerivedClassB]::Now() => $([DerivedClassB]::Now())"
"[DerivedClassC]::Now() => $([DerivedClassC]::Now())"
[BaseClass]::Now()     => 11/06/2023 09:41:23
[DerivedClassA]::Now() => 11/06/2023 09:41:23
[DerivedClassB]::Now() => 11/06/2023 09:41:23
[DerivedClassC]::Now() => 11/06/2023 09:41:23

다음 블록은 각 클래스의 DaysAgo() 정적 메서드를 호출해요. 기본 구현을 재정의한 DerivedClassB만 출력이 달라요.

"[BaseClass]::DaysAgo(3)     => $([BaseClass]::DaysAgo(3))"
"[DerivedClassA]::DaysAgo(3) => $([DerivedClassA]::DaysAgo(3))"
"[DerivedClassB]::DaysAgo(3) => $([DerivedClassB]::DaysAgo(3))"
"[DerivedClassC]::DaysAgo(3) => $([DerivedClassC]::DaysAgo(3))"
[BaseClass]::DaysAgo(3)     => 11/03/2023 09:41:38
[DerivedClassA]::DaysAgo(3) => 11/03/2023 09:41:38
[DerivedClassB]::DaysAgo(3) => 2023-11-03
[DerivedClassC]::DaysAgo(3) => 11/03/2023 09:41:38

다음 블록은 각 클래스의 새 인스턴스 문자열 표현을 보여줘요. ToString() 인스턴스 메서드를 재정의한 DerivedClassB의 표현이 달라요.

"`$base = [BaseClass]::new()     => $($base = [BaseClass]::new(); $base)"
"`$a    = [DerivedClassA]::new() => $($a = [DerivedClassA]::new(); $a)"
"`$b    = [DerivedClassB]::new() => $($b = [DerivedClassB]::new(); $b)"
"`$c    = [DerivedClassC]::new() => $($c = [DerivedClassC]::new(); $c)"
$base = [BaseClass]::new()     => 11/6/2023 9:44:57 AM
$a    = [DerivedClassA]::new() => 11/6/2023 9:44:57 AM
$b    = [DerivedClassB]::new() => 2023-11-06
$c    = [DerivedClassC]::new() => 11/6/2023 9:44:57 AM

다음 블록은 각 인스턴스에서 SetTimeStamp() 인스턴스 메서드를 호출해 TimeStamp 속성을 특정 날짜로 설정해요. 파생 클래스 중 매개 변수가 있는 오버로드를 재정의한 곳이 없어서 모든 인스턴스가 같은 날짜를 가져요.

[datetime]$Stamp = '2024-10-31'
"`$base.SetTimeStamp(`$Stamp) => $($base.SetTimeStamp($Stamp) ; $base)"
"`$a.SetTimeStamp(`$Stamp)    => $($a.SetTimeStamp($Stamp); $a)"
"`$b.SetTimeStamp(`$Stamp)    => $($b.SetTimeStamp($Stamp); $b)"
"`$c.SetTimeStamp(`$Stamp)    => $($c.SetTimeStamp($Stamp); $c)"
$base.SetTimeStamp($Stamp) => 10/31/2024 12:00:00 AM
$a.SetTimeStamp($Stamp)    => 10/31/2024 12:00:00 AM
$b.SetTimeStamp($Stamp)    => 2024-10-31
$c.SetTimeStamp($Stamp)    => 10/31/2024 12:00:00 AM

마지막 블록은 매개 변수 없이 SetTimeStamp()를 호출해요. 출력에서 DerivedClassC 인스턴스의 값이 다른 것들보다 10일 전으로 설정된 걸 볼 수 있어요.

"`$base.SetTimeStamp() => $($base.SetTimeStamp() ; $base)"
"`$a.SetTimeStamp()    => $($a.SetTimeStamp(); $a)"
"`$b.SetTimeStamp()    => $($b.SetTimeStamp(); $b)"
"`$c.SetTimeStamp()    => $($c.SetTimeStamp(); $c)"
$base.SetTimeStamp() => 11/6/2023 9:53:58 AM
$a.SetTimeStamp()    => 11/6/2023 9:53:58 AM
$b.SetTimeStamp()    => 2023-11-06
$c.SetTimeStamp()    => 10/27/2023 9:53:58 AM

Update-TypeData로 인스턴스 메서드 정의하기

클래스 정의에 메서드를 직접 선언하는 것 외에도, 정적 생성자에서 Update-TypeData cmdlet으로 클래스 인스턴스의 메서드를 정의할 수 있어요.

이 패턴의 시작점으로 다음 코드 조각을 쓰면 돼요. 꺾쇠 괄호 안의 자리 표시자 텍스트를 필요한 대로 바꿔 주세요.

class <ClassName> {
    static [hashtable[]] $MemberDefinitions = @(
        @{
            MemberName = '<MethodName>'
            MemberType = 'ScriptMethod'
            Value      = {
              param(<method-parameters>)

              <method-body>
            }
        }
    )

    static <ClassName>() {
        $TypeName = [<ClassName>].Name
        foreach ($Definition in [<ClassName>]::MemberDefinitions) {
            Update-TypeData -TypeName $TypeName @Definition
        }
    }
}

Add-Member cmdlet도 비정적 생성자에서 클래스에 속성과 메서드를 추가할 수 있지만, 이 cmdlet은 생성자가 호출될 때마다 실행돼요. 정적 생성자에서 Update-TypeData를 쓰면 클래스에 멤버를 추가하는 코드가 세션에서 한 번만 실행되도록 보장돼요.

기본 매개 변수 값과 검증 특성이 있는 메서드 정의하기

클래스 선언에 직접 정의한 메서드는 메서드 매개 변수에 기본값이나 검증 특성을 붙일 수 없어요. 기본값이나 검증 특성이 있는 클래스 메서드를 정의하려면 ScriptMethod 멤버로 정의해야 해요.

이 예제에서 CardDeck 클래스는 Count 매개 변수에 검증 특성과 기본값을 모두 사용하는 Draw() 메서드를 정의해요.

class CookieJar {
    [int] $Cookies = 12

    static [hashtable[]] $MemberDefinitions = @(
        @{
            MemberName = 'Eat'
            MemberType = 'ScriptMethod'
            Value      = {
                param(
                    [ValidateScript({ $_ -ge 1 -and $_ -le $this.Cookies })]
                    [int] $Count = 1
                )

                $this.Cookies -= $Count
                if ($Count -eq 1) {
                    "You ate 1 cookie. There are $($this.Cookies) left."
                } else {
                    "You ate $Count cookies. There are $($this.Cookies) left."
                }
            }
        }
    )

    static CookieJar() {
        $TypeName = [CookieJar].Name
        foreach ($Definition in [CookieJar]::MemberDefinitions) {
            Update-TypeData -TypeName $TypeName @Definition
        }
    }
}

$Jar = [CookieJar]::new()
$Jar.Eat(1)
$Jar.Eat()
$Jar.Eat(20)
$Jar.Eat(6)
You ate 1 cookie. There are 11 left.

You ate 1 cookie. There are 10 left.

MethodInvocationException:
Line |
  36 |  $Jar.Eat(20)
     |  ~~~~~~~~~~~~
     | Exception calling "Eat" with "1" argument(s): "The attribute
     | cannot be added because variable Count with value 20 would no
     | longer be valid."

You ate 6 cookies. There are 4 left.

참고 이 패턴이 검증 특성에서는 동작하지만, 예외 메시지가 "특성을 추가할 수 없다"고 나오는 게 좀 오해를 부를 수 있어요. 매개 변수의 값을 직접 확인하고 의미 있는 오류를 던지는 게 사용자 경험상 더 나을 수 있어요. 그래야 사용자들이 왜 오류가 났고 뭘 해야 하는지 이해할 수 있거든요.

제한 사항

PowerShell 클래스 메서드에는 다음과 같은 제한이 있어요.

  • 메서드 매개 변수는 검증 특성을 포함해 어떤 특성도 쓸 수 없어요.
    • 해결 방법: 메서드 본문에서 매개 변수를 검증 특성과 함께 다시 할당하거나, Update-TypeData cmdlet으로 정적 생성자에서 메서드를 정의해요.
  • 메서드 매개 변수는 기본값을 정의할 수 없고, 항상 필수예요.
    • 해결 방법: Update-TypeData cmdlet으로 정적 생성자에서 메서드를 정의해요.
  • 메서드는 숨겨도 항상 public이에요. 클래스가 상속될 때 재정의될 수 있어요.
    • 해결 방법: 없어요.
  • 메서드의 오버로드 중 하나라도 숨겨지면, 그 메서드의 모든 오버로드도 숨겨진 것으로 취급돼요.
    • 해결 방법: 없어요.

더 알아보기