about_Classes

about_Classes

PowerShell에서 클래스(class)를 활용해 나만의 사용자 지정 타입을 만드는 방법을 소개해 드릴게요. PowerShell 5.0부터 클래스를 정의할 수 있는 공식 문법이 생겼는데, 이 문법을 알면 개발자는 물론 IT 운영자도 훨씬 넓은 범위의 작업을 PowerShell로 처리할 수 있게 돼요. 이번 글에서는 기본 문법부터 속성·메서드·생성자, 그리고 static, hidden 같은 키워드와 상속, NoRunspaceAffinity 같은 심화 개념까지 차근차근 다뤄 볼게요.

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

본문

간단한 설명

클래스를 이용해서 나만의 사용자 지정 타입을 만드는 방법을 설명해 드릴게요.

자세한 설명

PowerShell 5.0부터는 클래스와 그 밖의 사용자 정의 타입을 정의할 수 있는 공식 문법이 도입됐어요. 클래스가 생기면서 개발자와 IT 운영자가 PowerShell을 훨씬 더 다양한 용도로 활용할 수 있게 됐죠.

클래스 선언(declaration)은 런타임에 객체의 인스턴스를 만들어 내는 설계도(blueprint) 역할을 해요. 클래스를 정의하면 그 클래스 이름이 곧 타입 이름이 돼요. 예를 들어 Device라는 클래스를 선언하고, 변수 $devDevice의 새 인스턴스로 초기화했다고 해 볼게요. 그러면 $devDevice 타입의 객체(인스턴스)가 돼요. Device의 각 인스턴스는 각자 서로 다른 속성 값을 가질 수 있답니다.

지원되는 시나리오

  • 클래스, 속성, 메서드, 상속 같은 객체 지향 프로그래밍 의미론을 이용해서 PowerShell에서 사용자 지정 타입을 정의할 수 있어요.
  • PowerShell 언어로 DSC 리소스와 그에 딸린 타입을 정의할 수 있어요.
  • 변수, 매개 변수, 사용자 지정 타입 정의를 꾸미기 위한 사용자 지정 특성(attribute)을 정의할 수 있어요.
  • 타입 이름으로 잡아낼 수 있는(catch) 사용자 지정 예외를 정의할 수 있어요.

문법

정의 문법

클래스 정의는 다음 문법을 따라요.

class <class-name> [: [<base-class>][,<interface-list>]] {
    [[<attribute>] [hidden] [static] <property-definition> ...]
    [<class-name>([<constructor-argument-list>])
      {<constructor-statement-list>} ...]
    [[<attribute>] [hidden] [static] <method-definition> ...]
}

인스턴스화 문법

클래스의 인스턴스를 만들려면 다음 문법 중 하나를 쓰면 돼요.

[$<variable-name> =] New-Object -TypeName <class-name> [
  [-ArgumentList] <constructor-argument-list>]
[$<variable-name> =] [<class-name>]::new([<constructor-argument-list>])
[$<variable-name> =] [<class-name>]<convertable-value-type>

참고: [<class-name>]::new() 문법을 쓸 때는 클래스 이름을 둘러싼 대괄호가 필수예요. 이 대괄호가 PowerShell에게 타입 정의임을 알려 주거든요.

<convertable-value-type> 문법은 매개 변수가 없는 기본 생성자(default constructor)를 가진 클래스에서만 동작해요. 이 문법은 기본 생성자로 클래스 인스턴스를 만든 뒤, 런타임 타입 변환을 이용해서 제공된 값들을 할당하는 방식이에요.

예제

예제 1 - 최소 정의

사용 가능한 클래스를 만들 때 필요한 최소한의 문법을 보여 주는 예제예요.

class Device {
    [string]$Brand
}

$dev = [Device]::new()
$dev.Brand = "Fabrikam, Inc."
$dev
Brand
-----
Fabrikam, Inc.

예제 2 - 인스턴스화 문법 사용하기

이 예제는 생성자 없이 속성만 여럿 가진 Book 클래스를 정의해요.

class Book {
    # Class properties
    [string]   $Title
    [string]   $Author
    [string]   $Synopsis
    [string]   $Publisher
    [datetime] $PublishDate
    [int]      $PageCount
    [string[]] $Tags
}

다음 예제는 기본 생성자가 타입 강제 변환(coercion)을 이용해 호환 가능한 값에서 속성 값을 채우는 모습을 보여 줘요. 여기서는 해시 테이블(hashtable)로 속성 값을 제공해요.

$Book1 = [Book] @{
    Title       = '1984'
    Author      = 'George Orwell'
    Synopsis    = ''
    Publisher   = 'Secker & Warburg'
    PublishDate = '1949-06-08'
    PageCount   = 328
    Tags        = @('Dystopian', 'Political Fiction', 'Social Science Fiction')
}
$Book1
Title       : 1984
Author      : George Orwell
Synopsis    :
Publisher   : Secker & Warburg
PublishDate : 6/8/1949 12:00:00 AM
PageCount   : 328
Tags        : {Dystopian, Political Fiction, Social Science Fiction}

해시 테이블의 키-값 쌍이 인스턴스 속성에 그대로 할당돼요. 만약 해시 테이블의 어떤 키가 유효한 속성 이름이 아니라면 인스턴스화가 실패해요. 이번에는 제네릭 리스트에 값들을 채워 주기 위해 배열을 사용한 예를 볼게요.

$List =  [System.Collections.Generic.List[int]] @(42, 43)
$List
42
43

예제 3 - 인스턴스 멤버를 가진 클래스

이 예제는 속성, 생성자, 메서드를 여럿 가진 Book 클래스를 정의해요. 정의된 모든 멤버는 인스턴스 멤버이지 정적 멤버가 아니에요. 이 속성들과 메서드들은 클래스로 만든 인스턴스를 통해서만 접근할 수 있어요.

class Book {
    # Class properties
    [string]   $Title
    [string]   $Author
    [string]   $Synopsis
    [string]   $Publisher
    [datetime] $PublishDate
    [int]      $PageCount
    [string[]] $Tags
    # Default constructor
    Book() { $this.Init(@{}) }
    # Convenience constructor from hashtable
    Book([hashtable]$Properties) { $this.Init($Properties) }
    # Common constructor for title and author
    Book([string]$Title, [string]$Author) {
        $this.Init(@{Title = $Title; Author = $Author })
    }
    # Shared initializer method
    [void] Init([hashtable]$Properties) {
        foreach ($Property in $Properties.Keys) {
            $this.$Property = $Properties.$Property
        }
    }
    # Method to calculate reading time as 2 minutes per page
    [timespan] GetReadingTime() {
        if ($this.PageCount -le 0) {
            throw 'Unable to determine reading time from page count.'
        }
        $Minutes = $this.PageCount * 2
        return [timespan]::new(0, $Minutes, 0)
    }
    # Method to calculate how long ago a book was published
    [timespan] GetPublishedAge() {
        if (
            $null -eq $this.PublishDate -or
            $this.PublishDate -eq [datetime]::MinValue
        ) { throw 'PublishDate not defined' }

        return (Get-Date) - $this.PublishDate
    }
    # Method to return a string representation of the book
    [string] ToString() {
        return "$($this.Title) by $($this.Author) ($($this.PublishDate.Year))"
    }
}

다음 코드 조각은 클래스의 인스턴스를 만들고 그것이 어떻게 동작하는지 보여 줘요. Book 클래스의 인스턴스를 만든 뒤, GetReadingTime()GetPublishedAge() 메서드를 이용해 책에 대한 메시지를 출력해요.

$Book = [Book]::new(@{
    Title       = 'The Hobbit'
    Author      = 'J.R.R. Tolkien'
    Publisher   = 'George Allen & Unwin'
    PublishDate = '1937-09-21'
    PageCount   = 310
    Tags        = @('Fantasy', 'Adventure')
})

$Book
$Time = $Book.GetReadingTime()
$Time = @($Time.Hours, 'hours and', $Time.Minutes, 'minutes') -join ' '
$Age  = [Math]::Floor($Book.GetPublishedAge().TotalDays / 365.25)

"It takes $Time to read $Book,`nwhich was published $Age years ago."
Title       : The Hobbit
Author      : J.R.R. Tolkien
Synopsis    :
Publisher   : George Allen & Unwin
PublishDate : 9/21/1937 12:00:00 AM
PageCount   : 310
Tags        : {Fantasy, Adventure}

It takes 10 hours and 20 minutes to read The Hobbit by J.R.R. Tolkien (1937),
which was published 86 years ago.

예제 4 - 정적 멤버를 가진 클래스

이 예제의 BookList 클래스는 앞선 예제의 Book 클래스를 바탕으로 만들어졌어요. BookList 클래스 자체를 static으로 표시할 수는 없지만, 구현부는 Books 정적 속성 하나와 그 속성을 관리하는 정적 메서드 묶음만 정의해요.

class BookList {
    # Static property to hold the list of books
    static [System.Collections.Generic.List[Book]] $Books
    # Static method to initialize the list of books. Called in the other
    # static methods to avoid needing to explicit initialize the value.
    static [void] Initialize()             { [BookList]::Initialize($false) }
    static [bool] Initialize([bool]$Force) {
        if ([BookList]::Books.Count -gt 0 -and -not $Force) {
            return $false
        }

        [BookList]::Books = [System.Collections.Generic.List[Book]]::new()

        return $true
    }
    # Ensure a book is valid for the list.
    static [void] Validate([book]$Book) {
        $Prefix = @(
            'Book validation failed: Book must be defined with the Title,'
            'Author, and PublishDate properties, but'
        ) -join ' '
        if ($null -eq $Book) { throw "$Prefix was null" }
        if ([string]::IsNullOrEmpty($Book.Title)) {
            throw "$Prefix Title wasn't defined"
        }
        if ([string]::IsNullOrEmpty($Book.Author)) {
            throw "$Prefix Author wasn't defined"
        }
        if ([datetime]::MinValue -eq $Book.PublishDate) {
            throw "$Prefix PublishDate wasn't defined"
        }
    }
    # Static methods to manage the list of books.
    # Add a book if it's not already in the list.
    static [void] Add([Book]$Book) {
        [BookList]::Initialize()
        [BookList]::Validate($Book)
        if ([BookList]::Books.Contains($Book)) {
            throw "Book '$Book' already in list"
        }

        $FindPredicate = {
            param([Book]$b)

            $b.Title -eq $Book.Title -and
            $b.Author -eq $Book.Author -and
            $b.PublishDate -eq $Book.PublishDate
        }.GetNewClosure()
        if ([BookList]::Books.Find($FindPredicate)) {
            throw "Book '$Book' already in list"
        }

        [BookList]::Books.Add($Book)
    }
    # Clear the list of books.
    static [void] Clear() {
      [BookList]::Initialize()
      [BookList]::Books.Clear()
    }
    # Find a specific book using a filtering scriptblock.
    static [Book] Find([scriptblock]$Predicate) {
        [BookList]::Initialize()
        return [BookList]::Books.Find($Predicate)
    }
    # Find every book matching the filtering scriptblock.
    static [Book[]] FindAll([scriptblock]$Predicate) {
        [BookList]::Initialize()
        return [BookList]::Books.FindAll($Predicate)
    }
    # Remove a specific book.
    static [void] Remove([Book]$Book) {
        [BookList]::Initialize()
        [BookList]::Books.Remove($Book)
    }
    # Remove a book by property value.
    static [void] RemoveBy([string]$Property, [string]$Value) {
        [BookList]::Initialize()
        $Index = [BookList]::Books.FindIndex({
            param($b)
            $b.$Property -eq $Value
        }.GetNewClosure())
        if ($Index -ge 0) {
            [BookList]::Books.RemoveAt($Index)
        }
    }
}

BookList가 정의됐으니 이제 앞선 예제의 책을 목록에 추가할 수 있어요.

$null -eq [BookList]::Books

[BookList]::Add($Book)

[BookList]::Books
True

Title       : The Hobbit
Author      : J.R.R. Tolkien
Synopsis    :
Publisher   : George Allen & Unwin
PublishDate : 9/21/1937 12:00:00 AM
PageCount   : 310
Tags        : {Fantasy, Adventure}

다음 코드 조각은 클래스의 정적 메서드를 호출해요.

[BookList]::Add([Book]::new(@{
    Title       = 'The Fellowship of the Ring'
    Author      = 'J.R.R. Tolkien'
    Publisher   = 'George Allen & Unwin'
    PublishDate = '1954-07-29'
    PageCount   = 423
    Tags        = @('Fantasy', 'Adventure')
}))

[BookList]::Find({
    param ($b)

    $b.PublishDate -gt '1950-01-01'
}).Title

[BookList]::FindAll({
    param($b)

    $b.Author -match 'Tolkien'
}).Title

[BookList]::Remove($Book)
[BookList]::Books.Title

[BookList]::RemoveBy('Author', 'J.R.R. Tolkien')
"Titles: $([BookList]::Books.Title)"

[BookList]::Add($Book)
[BookList]::Add($Book)
The Fellowship of the Ring

The Hobbit
The Fellowship of the Ring

The Fellowship of the Ring

Titles:

Exception:
Line |
  84 |              throw "Book '$Book' already in list"
     |              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | Book 'The Hobbit by J.R.R. Tolkien (1937)' already in list

예제 5 - Runspace 선호도가 있는 클래스와 없는 클래스 정의

[UnsafeClass]ShowRunspaceId() 메서드는 서로 다른 스레드 ID를 보고하지만 같은 runspace ID를 보고해요. 결국 세션 상태가 손상되어 Global scope cannot be removed 같은 오류가 발생할 수 있어요.

# Class definition with Runspace affinity (default behavior)
class UnsafeClass {
    static [Object] ShowRunspaceId($Val) {
        return [pscustomobject]@{
            ThreadId   = [Threading.Thread]::CurrentThread.ManagedThreadId
            RunspaceId = [runspace]::DefaultRunspace.Id
        }
    }
}

$unsafe = [UnsafeClass]::new()

while ($true) {
    1..10 | ForEach-Object -Parallel {
        Start-Sleep -ms 100
        ($Using:unsafe)::ShowRunspaceId($_)
    }
}

참고: 이 예제는 무한 루프로 실행돼요. 실행을 멈추려면 Ctrl+C를 누르면 돼요.

[SafeClass]ShowRunspaceId() 메서드는 서로 다른 스레드 ID와 Runspace ID를 보고해요.

# Class definition with NoRunspaceAffinity attribute
[NoRunspaceAffinity()]
class SafeClass {
    static [Object] ShowRunspaceId($Val) {
        return [pscustomobject]@{
            ThreadId   = [Threading.Thread]::CurrentThread.ManagedThreadId
            RunspaceId = [runspace]::DefaultRunspace.Id
        }
    }
}

$safe = [SafeClass]::new()

while ($true) {
    1..10 | ForEach-Object -Parallel {
        Start-Sleep -ms 100
        ($Using:safe)::ShowRunspaceId($_)
    }
}

참고: 이 예제는 무한 루프로 실행돼요. 실행을 멈추려면 Ctrl+C를 누르면 돼요.

클래스 속성

속성(properties)은 클래스 범위(scope) 안에서 선언된 변수예요. 속성은 어떤 기본 제공 타입이든, 혹은 다른 클래스의 인스턴스가든 될 수 있어요. 클래스는 속성을 0개 이상 가질 수 있고, 최대 속성 개수 제한은 없어요.

자세한 내용은 about_Classes_Properties 문서를 확인해 보세요.

클래스 메서드

메서드(methods)는 클래스가 수행할 수 있는 동작을 정의해요. 메서드는 입력 데이터를 지정하는 매개 변수를 가질 수 있어요. 메서드는 항상 출력 타입을 정의해요. 메서드가 출력을 반환하지 않는다면 Void 출력 타입을 가져야 해요. 메서드가 출력 타입을 명시적으로 정의하지 않으면 그 메서드의 출력 타입은 Void가 돼요.

자세한 내용은 about_Classes_Methods 문서를 확인해 보세요.

클래스 생성자

생성자(constructors)를 이용하면 클래스의 인스턴스를 만드는 순간에 기본 값을 설정하고 객체 논리를 검증할 수 있어요. 생성자는 클래스와 같은 이름을 가져요. 생성자는 새 객체의 데이터 멤버를 초기화하기 위해 매개 변수를 가질 수 있어요.

자세한 내용은 about_Classes_Constructors 문서를 확인해 보세요.

hidden 키워드

hidden 키워드는 클래스 멤버를 숨겨 줘요. 하지만 그 멤버는 여전히 사용자가 접근할 수 있고, 객체를 사용할 수 있는 모든 범위에서 계속 사용할 수 있어요. 숨겨진 멤버는 Get-Member cmdlet에서 드러나지 않고, 클래스 정의 바깥에서는 탭 완성이나 IntelliSense로도 표시되지 않아요.

hidden 키워드는 클래스 자체가 아니라 클래스 멤버에만 적용돼요.

숨겨진 클래스 멤버는 다음과 같은 특징이 있어요.

  • 클래스의 기본 출력에 포함되지 않아요.
  • Get-Member cmdlet이 반환하는 클래스 멤버 목록에 포함되지 않아요. Get-Member로 숨겨진 멤버를 보려면 Force 매개 변수를 쓰면 돼요.
  • 탭 완성이나 IntelliSense에 표시되지 않아요. 단, 완성이 그 숨겨진 멤버를 정의한 클래스 안에서 일어나는 경우는 예외예요.
  • 클래스의 공용(public) 멤버예요. 접근, 상속, 수정이 가능해요. 멤버를 숨긴다고 해서 private이 되는 건 아니에요. 위에서 설명한 내용대로 그 멤버가 표시되는 방식만 바뀔 뿐이에요.

참고: 메서드의 오버로드 중 하나를 숨기면 그 메서드는 IntelliSense, 완성 결과, Get-Member의 기본 출력에서 모두 제거돼요. 생성자를 하나라도 숨기면 IntelliSense와 완성 결과에서 new() 옵션이 제거돼요.

이 키워드에 대한 자세한 내용은 about_Hidden 문서를, 숨겨진 속성은 about_Classes_Properties 문서를, 숨겨진 메서드는 about_Classes_Methods 문서를, 숨겨진 생성자는 about_Classes_Constructors 문서를 확인해 보세요.

static 키워드

static 키워드는 클래스 안에 존재하면서 인스턴스가 필요 없는 속성이나 메서드를 정의해요.

정적 속성은 클래스를 인스턴스화하지 않아도 항상 사용할 수 있어요. 정적 속성은 클래스의 모든 인스턴스에 걸쳐 공유돼요. 정적 메서드도 항상 사용할 수 있어요. 모든 정적 속성은 세션 전체 기간 동안 살아 있어요.

static 키워드는 클래스 자체가 아니라 클래스 멤버에만 적용돼요.

정적 속성에 대한 자세한 내용은 about_Classes_Properties 문서를, 정적 메서드는 about_Classes_Methods 문서를, 정적 생성자는 about_Classes_Constructors 문서를 확인해 보세요.

PowerShell 클래스에서의 상속

기존 클래스에서 파생된 새 클래스를 만들어 클래스를 확장할 수 있어요. 파생된 클래스는 기본 클래스(base class)의 속성과 메서드를 상속받아요. 필요에 따라 기본 클래스의 멤버를 추가하거나 재정의(override)할 수 있어요.

PowerShell은 다중 상속을 지원하지 않아요. 클래스는 둘 이상의 클래스에서 직접 상속받을 수 없어요.

클래스는 계약(contract)을 정의하는 인터페이스(interface)에서도 상속받을 수 있어요. 인터페이스에서 상속받은 클래스는 그 계약을 반드시 구현해야 해요. 계약을 구현하면 그 클래스는 해당 인터페이스를 구현하는 다른 클래스처럼 사용할 수 있게 돼요.

기본 클래스에서 상속받거나 인터페이스를 구현하는 파생 클래스에 대한 자세한 내용은 about_Classes_Inheritance 문서를 확인해 보세요.

NoRunspaceAffinity 특성

Runspace는 PowerShell이 호출하는 명령의 실행 환경이에요. 이 환경에는 현재 존재하는 명령과 데이터, 그리고 현재 적용 중인 언어 제한이 모두 포함돼요.

기본적으로 PowerShell 클래스는 클래스가 만들어진 Runspace에 연결돼 있어요. 그래서 PowerShell 클래스를 ForEach-Object -Parallel에서 사용하는 것은 안전하지 않아요. 클래스에 대한 메서드 호출은 클래스가 만들어진 Runspace로 다시 마샬링되는데, 이 과정에서 Runspace의 상태가 손상되거나 교착 상태(deadlock)가 발생할 수 있어요.

클래스 정의에 NoRunspaceAffinity 특성을 추가하면 PowerShell 클래스가 특정 runspace에 연결되지 않도록 보장해 줘요. 그러면 인스턴스 메서드와 정적 메서드 호출 모두 실행 중인 스레드의 Runspace와 해당 스레드의 현재 세션 상태를 사용해요.

이 특성은 PowerShell 7.4에서 추가됐어요.

NoRunspaceAffinity 특성이 있는 클래스와 없는 클래스의 동작 차이를 보여 주는 예시는 예제 5를 참고하세요.

타입 액셀러레이터로 클래스 내보내기

기본적으로 PowerShell 모듈은 PowerShell 안에서 정의한 클래스와 열거형을 자동으로 내보내지 않아요. using module 문을 쓰지 않으면 사용자 지정 타입을 모듈 바깥에서 사용할 수 없어요.

하지만 모듈이 타입 액셀러레이터(type accelerators)를 추가하면, 사용자가 모듈을 가져온 직후에 그 타입 액셀러레이터를 바로 사용할 수 있어요.

참고: 세션에 타입 액셀러레이터를 추가하는 것은 내부(비공개) API를 사용해요. 이 API를 쓰면 충돌이 생길 수 있어요. 아래에서 설명하는 패턴은 모듈을 가져올 때 같은 이름의 타입 액셀러레이터가 이미 존재하면 오류를 던져요. 또 모듈을 세션에서 제거할 때 타입 액셀러레이터도 함께 제거해요.

이 패턴은 타입이 세션에서 사용 가능하도록 보장해 줘요. 하지만 VS Code에서 스크립트 파일을 작성할 때의 IntelliSense나 완성에는 영향을 주지 않아요. VS Code에서 사용자 지정 타입에 대한 IntelliSense와 완성 제안을 받으려면 스크립트 맨 위에 using module 문을 추가해야 해요.

다음 패턴은 모듈에서 PowerShell 클래스와 열거형을 타입 액셀러레이터로 등록하는 방법을 보여 줘요. 이 코드 조각을 루트 스크립트 모듈의 타입 정의 다음에 추가하면 돼요. $ExportableTypes 변수에 사용자에게 제공하고 싶은 타입이 모두 들어 있는지 확인해 주세요. 나머지 코드는 고칠 필요가 없어요.

# Define the types to export with type accelerators.
$ExportableTypes =@(
    [DefinedTypeName]
)
# Get the internal TypeAccelerators class to use its static methods.
$TypeAcceleratorsClass = [psobject].Assembly.GetType(
    'System.Management.Automation.TypeAccelerators'
)
# Ensure none of the types would clobber an existing type accelerator.
# If a type accelerator with the same name exists, throw an exception.
$ExistingTypeAccelerators = $TypeAcceleratorsClass::Get
foreach ($Type in $ExportableTypes) {
    if ($Type.FullName -in $ExistingTypeAccelerators.Keys) {
        $Message = @(
            "Unable to register type accelerator '$($Type.FullName)'"
            'Accelerator already exists.'
        ) -join ' - '

        throw [System.Management.Automation.ErrorRecord]::new(
            [System.InvalidOperationException]::new($Message),
            'TypeAcceleratorAlreadyExists',
            [System.Management.Automation.ErrorCategory]::InvalidOperation,
            $Type.FullName
        )
    }
}
# Add type accelerators for every exportable type.
foreach ($Type in $ExportableTypes) {
    $TypeAcceleratorsClass::Add($Type.FullName, $Type)
}
# Remove type accelerators when the module is removed.
$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = {
    foreach($Type in $ExportableTypes) {
        $TypeAcceleratorsClass::Remove($Type.FullName)
    }
}.GetNewClosure()

사용자가 모듈을 가져오면 세션의 타입 액셀러레이터에 추가된 타입들을 IntelliSense와 완성에서 바로 사용할 수 있어요. 모듈이 제거되면 타입 액셀러레이터도 함께 제거돼요.

PowerShell 모듈에서 클래스 직접 가져오기

Import-Module#Requires 문은 모듈이 정의한 대로 모듈의 함수, 별칭, 변수만 가져와요. 클래스는 가져오지 않아요.

모듈이 클래스와 열거형을 정의했지만 그 타입에 대한 타입 액셀러레이터를 추가하지 않았다면, using module 문을 사용해 가져오면 돼요.

using module 문은 스크립트 모듈이나 이진 모듈의 루트 모듈(ModuleToProcess)에서 클래스와 열거형을 가져와요. 중첩 모듈에 정의된 클래스나 루트 모듈에 점 소싱(dot-sourced)된 스크립트에 정의된 클래스는 일관되게 가져오지 않을 수 있어요. 모듈 바깥의 사용자가 사용할 수 있게 하고 싶은 클래스는 루트 모듈에 직접 정의해 주세요.

using 문에 대한 자세한 내용은 about_Using 문서를 확인해 보세요.

개발 중에 새로 바뀐 코드 불러오기

스크립트 모듈을 개발할 때는 코드를 수정한 뒤 Force 매개 변수를 쓴 Import-Module로 새 버전의 모듈을 불러오는 일이 흔해요. 하지만 모듈을 다시 불러오는 것은 루트 모듈의 함수 변경에만 동작해요. Import-Module은 중첩 모듈을 다시 불러오지 않아요. 또 바뀐 클래스를 불러올 방법도 없어요.

최신 버전을 실행 중인지 확실히 하려면 새 세션을 시작해야 해요. PowerShell에서 정의했고 using 문으로 가져온 클래스와 열거형은 언로드할 수 없어요.

또 하나 흔한 개발 방식은 코드를 여러 파일로 나누는 거예요. 한 파일의 함수가 다른 모듈에 정의된 클래스를 사용한다면, 그 함수가 필요한 클래스 정의를 갖도록 using module 문을 사용해야 해요.

PSReference 타입은 클래스 멤버에서 지원되지 않아요

[ref] 타입 액셀러레이터는 PSReference 클래스의 축약형이에요. [ref]로 클래스 멤버를 타입 캐스팅하면 조용히 실패해요. [ref] 매개 변수를 사용하는 API는 클래스 멤버에 사용할 수 없어요. PSReference 클래스는 COM 객체를 지원하기 위해 설계됐어요. COM 객체에는 값을 참조로 전달해야 하는 경우가 있거든요.

자세한 내용은 PSReference Class 문서를 확인해 보세요.

제한 사항

다음 목록은 PowerShell 클래스를 정의할 때의 제한 사항과, 있을 경우 그에 대한 해결 방법을 담고 있어요.

일반 제한 사항

  • 클래스 멤버는 자신의 타입으로 PSReference를 사용할 수 없어요.
    • 해결 방법: 없음.
  • PowerShell 클래스는 세션에서 언로드하거나 다시 로드할 수 없어요.
    • 해결 방법: 새 세션을 시작하세요.
  • 모듈에 정의된 PowerShell 클래스는 자동으로 가져와지지 않아요.
    • 해결 방법: 정의된 타입을 루트 모듈의 타입 액셀러레이터 목록에 추가하세요. 그러면 모듈을 가져올 때 그 타입을 사용할 수 있게 돼요.
  • hiddenstatic 키워드는 클래스 정의가 아니라 클래스 멤버에만 적용돼요.
    • 해결 방법: 없음.
  • 기본적으로 PowerShell 클래스는 runspace를 가로지르는 병렬 실행에 안전하지 않아요. 클래스의 메서드를 호출하면 PowerShell이 그 호출을 클래스가 만들어진 Runspace로 마샬링하는데, 이 과정에서 Runspace의 상태가 손상되거나 교착 상태가 발생할 수 있어요.
    • 해결 방법: 클래스 선언에 NoRunspaceAffinity 특성을 추가하세요.

생성자 제한 사항

  • 생성자 체이닝(chaining)은 구현되지 않았어요.
    • 해결 방법: 숨겨진 Init() 메서드를 정의하고 생성자 안에서 호출하세요.
  • 생성자 매개 변수는 검증 특성을 포함한 어떤 특성도 사용할 수 없어요.
    • 해결 방법: 생성자 본문에서 검증 특성을 붙여 매개 변수를 다시 할당하세요.
  • 생성자 매개 변수는 기본 값을 정의할 수 없어요. 매개 변수는 항상 필수예요.
    • 해결 방법: 없음.
  • 생성자의 어떤 오버로드라도 숨겨지면, 그 생성자의 모든 오버로드가 숨겨진 것으로 처리돼요.
    • 해결 방법: 없음.

메서드 제한 사항

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

속성 제한 사항

  • 정적 속성은 항상 변경 가능(mutable)해요. PowerShell 클래스는 불변(immutable) 정적 속성을 정의할 수 없어요.
    • 해결 방법: 없음.
  • 속성은 ValidateScript 특성을 사용할 수 없어요. 클래스 속성 특성의 인수는 상수여야 하기 때문이에요.
    • 해결 방법: ValidateArgumentsAttribute 타입에서 상속받는 클래스를 정의하고 그 특성을 대신 사용하세요.
  • 직접 선언된 속성은 사용자 지정 getter와 setter 구현을 정의할 수 없어요.
    • 해결 방법: 숨겨진 속성을 정의하고 Update-TypeData로 보이는 getter와 setter 로직을 정의하세요.
  • 속성은 Alias 특성을 사용할 수 없어요. 이 특성은 매개 변수, cmdlet, 함수에만 적용되거든요.
    • 해결 방법: Update-TypeData cmdlet을 사용해 클래스 생성자 안에서 별칭을 정의하세요.
  • PowerShell 클래스를 ConvertTo-Json cmdlet으로 JSON으로 변환하면, 출력 JSON에는 모든 숨겨진 속성과 그 값이 포함돼요.
    • 해결 방법: 없음.

상속 제한 사항

  • PowerShell은 스크립트 코드에서 인터페이스를 정의하는 것을 지원하지 않아요.
    • 해결 방법: C#으로 인터페이스를 정의하고 그 인터페이스를 정의하는 어셈블리를 참조하세요.
  • PowerShell 클래스는 하나의 기본 클래스에서만 상속받을 수 있어요.
    • 해결 방법: 클래스 상속은 전이적(transitive)이에요. 파생 클래스가 다른 파생 클래스에서 상속받아 기본 클래스의 속성과 메서드를 얻을 수 있어요.
  • 제네릭 클래스나 인터페이스에서 상속받을 때, 제네릭의 타입 매개 변수는 이미 정의되어 있어야 해요. 클래스가 자기 자신을 클래스나 인터페이스의 타입 매개 변수로 정의할 수는 없어요.
    • 해결 방법: 제네릭 기본 클래스나 인터페이스에서 파생하려면 사용자 지정 타입을 다른 .psm1 파일에 정의하고 using module 문으로 그 타입을 불러오세요. 제네릭에서 상속받을 때 사용자 지정 타입이 자기 자신을 타입 매개 변수로 쓰는 데는 해결 방법이 없어요.

더 알아보기