about_Member-Access_Enumeration — 컬렉션에서 멤버 액세스 자동 열거하기

about_Member-Access_Enumeration — 컬렉션에서 멤버 액세스 자동 열거하기

PowerShell은 컬렉션의 각 항목에 일일이 손을 대지 않아도, 점(.) 연산자 하나로 컬렉션 전체에 멤버를 적용할 수 있게 해줘요. 이 문서는 그 **멤버 액세스 열거(member-access enumeration)**가 언제, 어떻게 동작하는지 — 그리고 예상 밖의 결과가 나오는 까다로운 경우는 어떤 게 있는지 — 차근차근 짚어드릴게요.

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

본문

간단한 설명

멤버 액세스 연산자를 쓸 때 컬렉션을 자동으로 열거(enumerate)하는 동작을 설명하는 문서예요.

자세한 설명

PowerShell은 열거 가능한(enumerable) 타입들의 목록을 유지하고 있어요. PowerShell 3.0부터 도입된 멤버 액세스 열거 기능 덕분에, 열거 가능한 컬렉션 객체에 멤버 액세스 연산자(.)를 쓰는 게 훨씬 편리해졌어요.

멤버 액세스 열거를 쓰면 코드를 더 간단하고 짧게 작성할 수 있어요. 컬렉션을 ForEach-Object에 파이프로 넘기거나, 컬렉션의 각 항목에 멤버를 적용하려고 ForEach() 내장 메서드를 사용하는 대신, 컬렉션 객체에 멤버 액세스 연산자를 바로 쓸 수 있거든요.

아래 예시들은 모두 같은 결과를 내요. 마지막 예시가 멤버 액세스 연산자를 쓰는 방법이에요.

PS> Get-Service -Name event* | ForEach-Object -Process { $_.DisplayName }
Windows Event Log
COM+ Event System
PS> (Get-Service -Name event*).ForEach({ $_.DisplayName })
Windows Event Log
COM+ Event System
PS> (Get-Service -Name event*).DisplayName
Windows Event Log
COM+ Event System

참고: 멤버 액세스 연산자로 컬렉션 항목들의 속성 을 가져올 수는 있지만, 그 값들을 직접 설정할 수는 없어요. 자세한 내용은 about_Arrays를 참고하세요. 멤버 액세스 열거는 편의를 위한 기능이라, 여러 열거 방법 사이에는 미묘한 동작·성능 차이가 있을 수 있다는 점도 기억해두면 좋아요.

객체에 멤버 액세스 연산자를 썼을 때 그 멤버가 객체에 존재하면, 그 멤버가 호출돼요. 그런데 지정한 멤버가 없는 컬렉션 객체에 연산자를 쓰면, PowerShell은 그 컬렉션의 항목들을 열거하면서 열거된 각 항목에 멤버 액세스 연산자를 적용해요.

  • 속성에 대해 열거할 때, 연산자는 해당 속성을 가진 각 항목의 속성 값을 돌려줘요. 그 속성을 가진 항목이 하나도 없으면 $null을 반환해요.
  • 메서드에 대해 열거할 때, 연산자는 컬렉션의 각 항목에서 그 메서드를 호출하려고 시도해요. 어느 항목이라도 해당 메서드가 없으면 MethodNotFound 예외를 반환해요.

경고: 메서드에 대한 멤버 액세스 열거는 컬렉션의 각 항목에서 메서드를 호출해요. 호출하는 메서드가 값을 바꾸는 작업이라면, 그 변경이 컬렉션의 모든 항목에 적용돼요. 열거 도중 오류가 나면, 오류가 나기 전까지 열거된 항목에만 메서드가 호출돼요. 더 안전하게 처리하려면 항목을 직접 열거하면서 오류를 명시적으로 처리하는 걸 고려해보세요.

열거 불가능한 객체의 멤버 액세스

열거 가능한 컬렉션이 아닌 객체에 멤버 액세스 연산자를 쓰면, PowerShell은 그 멤버를 호출해 속성 값을 돌려주거나 메서드 출력을 반환해요.

PS> $MyString = 'abc'
PS> $MyString.Length
3
PS> $MyString.ToUpper()
ABC

멤버가 없는 열거 불가능한 객체에 멤버 액세스 연산자를 쓰면, 빠진 속성은 $null을, 빠진 메서드는 MethodNotFound 오류를 반환해요.

PS> $MyString = 'abc'
PS> $null -eq $MyString.DoesNotExist
True
PS> $MyString.DoesNotExist()
InvalidOperation: Method invocation failed because [System.String] does not contain a method named 'DoesNotExist'.

컬렉션 객체의 멤버 액세스

지정한 멤버가 컬렉션 객체 자체에 있을 때 멤버 액세스 연산자를 쓰면, 항상 그 컬렉션 객체의 속성 값이나 메서드 결과를 돌려줘요.

컬렉션에는 있지만 항목에는 없는 멤버 액세스

이 예시에서는 지정한 멤버가 컬렉션에는 존재하지만, 그 안의 항목에는 존재하지 않아요.

PS> [System.Collections.Generic.List[string]]$Collection = @('a', 'b')
PS> $Collection.IsReadOnly
False
PS> $Collection.Add('c')
PS> $Collection
a
b
c

컬렉션과 항목 양쪽 모두에 있는 멤버 액세스

이 예시에서는 지정한 멤버가 컬렉션과 그 항목 양쪽에 모두 존재해요. 컬렉션에 멤버 액세스 연산자를 쓴 결과와, ForEach-Object에서 컬렉션 항목들에 멤버 액세스 연산자를 쓴 결과를 비교해보세요. 컬렉션에서는 연산자가 그 컬렉션 객체의 속성 값이나 메서드 결과를 돌려주지, 항목들의 결과는 돌려주지 않아요.

PS> [System.Collections.Generic.List[string]]$Collection = @('a', 'b', 'c')
PS> $Collection.Count
3
PS> $Collection | ForEach-Object -Process { $_.Count }
1
1
1
PS> $Collection.ToString()
System.Collections.Generic.List`1[System.String]
PS> $Collection | ForEach-Object -Process { $_.ToString() }
a
b
c

참고: System.Collections.IDictionary 인터페이스를 구현하는 컬렉션 — 예를 들어 HashTable이나 OrderedDictionary — 은 동작이 달라요. 사전에 속성과 같은 이름의 키가 있으면, 멤버 액세스 연산자를 쓸 때 속성 값 대신 그 키의 값을 돌려줘요.

사전 객체의 속성 값에는 psbase 내장 멤버로 접근할 수 있어요. 키 이름이 keys일 때 HashTable 키의 컬렉션을 돌려받고 싶다면, 이렇게 쓰면 돼요:

$hashtable.psbase.Keys

컬렉션의 모든 항목에는 있지만 컬렉션 자체에는 없는 멤버 액세스

지정한 멤버가 컬렉션 객체에는 없지만 그 안의 항목들에는 있는 경우, 멤버 액세스 연산자를 쓰면 PowerShell이 컬렉션의 항목들을 열거해 각 항목의 속성 값이나 메서드 결과를 돌려줘요.

PS> [System.Collections.Generic.List[string]]$Collection = @('a', 'b', 'c')
PS> $Collection.Length
1
1
1
PS> $Collection.ToUpper()
A
B
C

컬렉션에도 항목에도 없는 멤버 액세스

지정한 멤버가 컬렉션 객체에도 없고 그 항목들에도 없을 때 멤버 액세스 연산자를 쓰면, 속성을 지정했을 때는 $null을, 메서드를 지정했을 때는 MethodNotFound 오류를 반환해요.

PS> [System.Collections.Generic.List[string]]$Collection = @('a', 'b', 'c')
PS> $null -eq $Collection.DoesNotExist
True
PS> $Collection.DoesNotExist()
InvalidOperation: Method invocation failed because [System.String] does not
contain a method named 'DoesNotExist'.

컬렉션 객체에 이 멤버가 없으니 PowerShell이 그 안의 항목들을 열거했어요. 여기서 MethodNotFound 오류가 System.Collections.Generic.List가 아니라 System.String이 그 메서드를 포함하지 않는다고 알려준다는 점을 눈여겨보세요.

컬렉션의 일부 항목에만 있는 메서드 액세스

컬렉션 객체에는 없고 항목 중 일부에만 존재하는 메서드에 멤버 액세스 연산자로 접근하면, 그 메서드가 없는 첫 번째 항목에서 MethodNotFound 오류를 반환해요. 일부 항목에서는 메서드가 실제로 호출되더라도, 명령은 오류만 반환해요.

PS> @('a', 1, 'c').ToUpper()
InvalidOperation: Method invocation failed because [System.Int32] does not
contain a method named 'ToUpper'.

컬렉션의 일부 항목에만 있는 속성 액세스

컬렉션 객체에는 없고 항목 중 일부에만 존재하는 속성에 멤버 액세스 연산자로 접근하면, 그 속성을 가진 컬렉션의 각 항목에 대해 속성 값을 돌려줘요.

PS> $CapitalizedProperty = @{
    MemberType = 'ScriptProperty'
    Name       = 'Capitalized'
    Value      = { $this.ToUpper() }
    PassThru   = $true
}
PS> [System.Collections.Generic.List[Object]]$MixedCollection = @(
    'a'
    ('b' | Add-Member @CapitalizedProperty)
    ('c' | Add-Member @CapitalizedProperty)
    'd'
)
PS> $MixedCollection.Capitalized
B
C

중첩 컬렉션의 멤버 액세스

열거 가능한 컬렉션이 중첩 컬렉션을 포함하고 있으면, 각 중첩 컬렉션에도 멤버 액세스 열거가 적용돼요.

예를 들어 $a는 문자열 배열이 중첩된 배열과 문자열 하나, 이렇게 두 요소를 담은 배열이라고 해볼게요.

# Get the count of items in the array.
PS> $a.Count
2
# Get the count of items in each nested item.
PS> $a.GetEnumerator().Count
2
1
# Call the ToUpper() method on all items in the nested array.
PS> $a = @(, ('bar', 'baz'), 'foo')
PS> $a.ToUpper()
BAR
BAZ
FOO

멤버 액세스 연산자를 쓰면 PowerShell이 $a의 항목들을 열거하면서 모든 항목에 ToUpper() 메서드를 호출해요.

알아두면 좋을 점

앞서 말했듯이, 여러 열거 방법 사이에는 미묘한 동작·성능 차이가 있어요.

오류가 나면 출력이 사라져요

멤버 액세스 열거가 오류로 중단되면, 오류가 나기 전까지 성공했던 메서드 호출의 출력도 반환되지 않아요. 열거를 종료시키는 조건은 이렇습니다:

  • 열거된 객체가 접근하려는 메서드를 갖고 있지 않다
  • 접근하는 메서드가 종료 오류(terminating error)를 일으킨다

다음 예시를 함께 볼게요:

class Class1 { [Object] Foo() { return 'Bar' } }
class Class2 { [void] Foo() { throw 'Error' } }
class Class3 {}

$example1 = ([Class1]::new(), [Class1]::new())
$example2 = ([Class1]::new(), [Class2]::new())
$example3 = ([Class1]::new(), [Class3]::new())

$example1의 두 항목 모두 Foo() 메서드를 갖고 있으므로, 메서드 호출이 성공해요.

PS> $example1.Foo()
Bar
Bar

$example2의 두 번째 항목에 있는 Foo() 메서드는 오류를 던지므로, 열거가 실패해요.

PS> $example2.Foo()
Exception:
Line |
   2 |  class Class2 { [void] Foo() { throw 'Error' } }
     |                                ~~~~~~~~~~~~~
     | Error

$example2의 두 번째 항목은 Foo() 메서드가 없으므로, 열거가 실패해요. (표시된 오류가 Class2의 throw가 아니라 메서드 부재로 보이는 부분이 혼동될 수 있는데, 이어지는 예시를 보면 더 명확해져요.)

PS> $example3.Foo()
InvalidOperation: Method invocation failed because [Class3] does not contain
a method named 'Foo'.

이걸 ForEach-Object로 열거한 것과 비교해볼게요.

PS> $example2 | ForEach-Object -MemberName Foo
Bar
ForEach-Object: Exception calling "Foo" with "0" argument(s): "Error"
PS> $example3 | ForEach-Object -MemberName Foo
Bar

여기서는 배열의 첫 번째 항목에 대한 Foo() 호출이 성공했다는 출력이 보여요.

PSCustomObject 인스턴스를 담은 컬렉션

객체 컬렉션이 PSCustomObject 항목 인스턴스를 포함하고 있으면, 접근한 속성이 누락됐을 때 PowerShell이 예상 밖으로 $null 값을 돌려줘요.

아래 예시들에서는 참조된 속성을 가진 객체가 하나 이상 존재해요.

PS> $foo = [pscustomobject]@{ Foo = 'Foo' }
PS> $bar = [pscustomobject]@{ Bar = 'Bar' }
PS> $baz = [pscustomobject]@{ Baz = 'Baz' }
PS> ConvertTo-Json ($foo, $bar, $baz).Foo
[\
  "Foo",\
  null,\
  null\
]
PS> ConvertTo-Json ((Get-Process -Id $PID), $foo).Name
[\
  "pwsh",\
  null\
]

지정한 속성을 가진 항목에 대해 단일 객체를 반환할 거라 예상하셨겠지만, PowerShell은 그 속성이 없는 항목마다 $null 값도 함께 반환해요.

이 동작에 대한 자세한 내용은 PowerShell 이슈 #13752를 참고하세요.

더 알아보기