about_Type_Conversion — PowerShell 타입 변환
about_Type_Conversion — PowerShell 타입 변환
PowerShell의 타입 시스템은 유연해서 참 편리해요. 그런데 이 유연함 덕분에 예상치 못한 결과에 부딪히는 경우가 많죠. 이번 글에서는 PowerShell이 값을 어떤 타입으로, 어떻게 바꾸는지 하나씩 짚어볼게요. 코드와 식별자는 원문 그대로라 그대로 따라 해 보시면 돼요.
출처: about_Type_Conversion (Microsoft Learn)
본문
짧은 설명
PowerShell에는 사용하기 편하도록 설계된 유연한 타입 시스템이 있어요. 다만 예상치 못한 결과를 피하려면 그 동작 방식을 이해하고 있어야 해요.
자세한 설명
기본적으로 PowerShell 변수는 타입이 고정되어 있지 않아요. 한 타입의 값을 담은 변수를 만들었다가 나중에 다른 타입의 값을 얼마든지 다시 할당할 수 있죠. 게다가 PowerShell은 명시적으로든 암시적으로든 값을 다른 타입으로 자동 변환하기도 해요. 암시적 타입 변환은 꽤 유용하지만, 타입 처리가 엄격한 언어에 익숙한 분이라면 여기서 헷갈리기 쉬운 함정들이 있어요.
타입 제한 변수와 명시적 타입 변환
변수에 타입을 고정하고 싶다면, 할당문에서 변수 이름 왼쪽에 타입 리터럴을 붙여 주면 돼요. 예를 들면 이렇게요.
[int]$foo = 42
타입 캐스트를 쓰면 값을 특정 타입으로 명시적으로 변환할 수도 있어요.
PS> $var = [int]'43'
PS> $var.GetType().Name
Int32
타입 제한을 걸어 두면 지정한 타입의 값만 그 변수에 할당할 수 있어요. 만약 제한된 타입으로 변환 가능한 다른 타입의 값을 할당하려 하면, PowerShell이 암시적 변환을 수행해요. 자세한 내용은 이 글의 암시적 타입 변환 섹션을 참고해 주세요.
숫자 타입 변환
숫자 타입끼리는, 목표 타입이 변환된 값을 담을 수 있기만 하면 어떤 숫자 타입으로든 변환할 수 있어요.
PS> (42.1).GetType().Name
Double
PS> $byte = [byte] 42.1
PS> $byte
42
PS> $byte.GetType().Name
Byte
42.1은 Double 타입이에요. 이걸 Byte로 캐스트하면 PowerShell이 정수 42로 잘라내는데, 그 값은 Byte에 담기에 충분히 작죠.
실수를 정수 타입으로 변환할 때 PowerShell은 잘라내기가 아니라 반올림을 사용해요. 특히 rounding-to-nearest-even(가장 가까운 짝수로 반올림) 방식을 쓰죠. 아래 예시를 보면 두 값 모두 가장 가까운 짝수인 22로 반올림되는 걸 확인할 수 있어요.
PS> [byte]21.5
22
PS> [byte]22.5
22
더 자세한 내용은 Math.Round 메서드의 Midpoint values and rounding conventions 섹션을 참고해 주세요.
부울 타입 변환
어떤 타입의 값이든 Boolean으로 강제할 수 있어요.
- 숫자 타입에서는
0이$false로 바뀌고, 다른 모든 값은$true로 바뀌어요.
PS> [boolean]0
False
PS> [boolean]0.0
False
PS> [boolean]-1
True
PS> [boolean]1
True
PS> [boolean]42.1
True
- 다른 타입에서는 null 값, 빈 문자열, 빈 배열이
$false로 바뀌어요.
PS> [boolean]''
False
PS> [boolean]@()
False
PS> [boolean]'Hello'
True
빈 해시테이블을 포함한 그 외의 값은 $true로 바뀌어요. 요소가 하나뿐인 컬렉션은 그 단일 요소의 Boolean 값으로 평가되고, 요소가 둘 이상인 컬렉션은 항상 $true예요.
PS> [boolean]@(0)
False
PS> [boolean]@(0,0)
True
PS> [boolean]@{}
True
문자열 타입 변환
어떤 타입의 값이든 String으로 강제할 수 있어요. 기본 변환은 객체의 ToString() 메서드를 호출하는 방식이에요.
배열은 문자열로 변환돼요. 배열의 각 요소는 개별적으로 문자열로 변환된 뒤 하나로 이어지죠. 기본적으로 변환된 값들은 공백으로 구분되는데, 이 구분자는 $OFS 기본 설정 변수를 바꾸면 조정할 수 있어요.
PS> [string] @(1, 2, 3)
1 2 3
$OFS에 대한 자세한 내용은 about_Preference_Variables를 참고해 주세요.
타입이 정적 Parse() 메서드를 구현하고 있다면 단일 문자열 값을 그 타입의 인스턴스로 변환할 수 있어요. 예를 들어 [bigint]'42'는 [bigint]::Parse('42', [cultureinfo]::InvariantCulture)와 같아요. 여기서 선택적으로 붙는 [cultureinfo]::InvariantCulture 값은 메서드의 IFormatProvider 타입 파라미터에 바인딩되면서, 변환 동작을 문화권에 영향받지 않게 보장해 줘요. 다만 모든 Parse() 메서드 구현이 이 파라미터를 갖고 있는 건 아니에요.
참고: 문자열과의 변환은 보통 고정 문화권(Invariant culture)을 사용해 수행돼요. 고정 문화권은 미국식 영어 문화권에 기반을 두되 동일하지는 않은데, 대표적으로 소수 구분자로 마침표(.)를 쓰고 기본적으로 미국식(월-우선) 날짜를 쓴다는 점이 달라요. 다만 이진 cmdlet은 파라미터 바인딩 과정에서 문화권에 민감한 변환을 수행해요.
열거형 타입 변환
PowerShell은 Enum 타입을 String 인스턴스와 서로 변환할 수 있어요. 예를 들어 타입 캐스트 문자열 [System.PlatformId]'Unix'는 열거형 값 [System.PlatformId]::Unix와 같아요. PowerShell은 플래그 기반 열거형도 문자열 안의 쉼표로 구분된 값이나 문자열 배열로 올바르게 처리해 줘요. 다음 예시를 볼까요.
[System.Reflection.TypeAttributes]'Public, Abstract'
[System.Reflection.TypeAttributes]('Public', 'Abstract')
이 예시들은 아래의 열거형 표현식과 같은 의미예요.
[System.Reflection.TypeAttributes]::Public -bor
[System.Reflection.TypeAttributes]::Abstract
기타 타입 변환
단일 값(비배열)은 다음 두 조건을 모두 만족할 때 어떤 타입의 인스턴스로 변환할 수 있어요.
-
그 타입에 (공개) 단일 파라미터 생성자가 있고
-
그 값이 파라미터 타입과 같은 타입이거나 그 타입으로 강제될 수 있을 때
예를 들어 아래 두 줄은 서로 동등해요.
[regex]'a|b'
[regex]::new('a|b')`
암시적 타입
PowerShell은 리터럴 값에도 타입을 자동으로 부여해요.
숫자 리터럴은 기본적으로 크기에 따라 타입이 정해져요. 42는 Int32 타입으로 저장할 수 있을 만큼 작고, 1.2는 Double로 저장돼요. [int32]::MaxValue보다 큰 정수는 Int64로 저장되죠. 42는 Byte로, 1.2는 Single로도 저장될 수 있지만, 암시적 타입 지정은 각각 Int32와 Double을 사용한다는 점을 기억해 두세요. 자세한 내용은 about_Numeric_Literals를 참고해 주세요.
문자열 리터럴은 암시적으로 String 타입이 돼요. 단일 문자 String 인스턴스는 Char 타입과 서로 변환할 수 있는데, PowerShell에는 리터럴 형태의 Char 타입이 없다는 점은 알아 두세요.
암시적 타입 변환
특정 상황에서 PowerShell은 값을 다른 타입으로 암시적으로 변환할 수 있어요. 그런 상황은 다음과 같아요.
-
파라미터 바인딩
-
타입 제한 변수
-
연산자를 사용하는 표현식
-
부울 컨텍스트 — PowerShell은
if,while,do,switch문의 조건식을 앞서 설명한 대로 Boolean 값으로 변환해요. 자세한 내용은 about_Booleans를 참고해 주세요. -
ETS(Extended Type System) 타입 정의 — 타입 변환은 여러 방식으로 정의할 수 있어요.
-
Update-TypeData의TypeConverter파라미터 사용 -
Types.ps1xml 파일에서
-
TypeConverterAttribute특성으로 데코레이션된 타입의 컴파일된 코드에서 -
TypeConverter또는PSTypeConverter에서 파생된 클래스를 통해서
-
파라미터 바인딩 변환
PowerShell은 파라미터에 전달된 값을 파라미터 타입에 맞추려 시도해요. 파라미터 값의 타입 변환은 파라미터가 특정 타입으로 선언된 cmdlet, 함수, 스크립트, 스크립트블록 또는 .NET 메서드에서 일어나요. 파라미터를 [Object] 타입으로 선언하거나 특정 타입을 정하지 않으면 어떤 값 타입이든 파라미터에 전달할 수 있어요. 또 ArgumentTransformationAttribute 특성으로 파라미터를 데코레이션해 사용자 지정 변환을 정의할 수도 있어요.
자세한 내용은 about_Parameter_Binding을 참고해 주세요.
파라미터 바인딩 모범 사례
.NET 메서드에서는 필요한 곳에 타입 캐스트를 써서 예상되는 타입을 정확히 전달하는 편이 좋아요. 정확한 타입이 없으면 PowerShell이 잘못된 메서드 오버로드를 고를 수 있어요. 게다가 향후 .NET 버전에서 새 메서드 오버로드가 추가되면 기존 코드가 깨질 수도 있죠. 이 문제의 극단적인 예시는 이 Stack Overflow 질문에서 볼 수 있어요.
[string] 타입으로 선언된 파라미터에 배열을 전달하면, PowerShell이 앞서 설명한 대로 배열을 문자열로 변환할 수 있어요. 기본 함수 하나를 볼게요.
function Test-String {
param([string] $String)
$String
}
Test-String -String 1, 2
이 함수는 배열이 문자열로 변환되기 때문에 1 2를 출력해요. 이 동작을 피하려면 [CmdletBinding()] 특성을 추가해 고급 함수(advanced function)로 만들면 돼요.
function Test-String {
[CmdletBinding()]
param([string] $String)
$String
}
Test-String -String 1, 2
고급 함수에서는 PowerShell이 배열을 비배열 타입에 바인딩하지 않아요. 배열을 전달하면 다음과 같은 오류 메시지가 반환돼요.
Test-String:
Line |
7 | Test-String -String 1, 2
| ~~~~
| Cannot process argument transformation on parameter 'String'. Cannot
| convert value to type System.String.
안타깝게도 .NET 메서드 호출에서는 이 동작을 피할 방법이 없어요.
PS> (Get-Date).ToString(@(1, 2))
1 2
PowerShell이 배열을 문자열 "1 2"로 변환해서 ToString() 메서드의 Format 파라미터로 전달해요.
다음 예시는 배열 변환 문제의 또 다른 사례를 보여줘요.
PS> $bytes = [byte[]] @(1..16)
PS> $guid = New-Object System.Guid($bytes)
New-Object: Cannot find an overload for "Guid" and the argument count: "16".
PowerShell은 $bytes가 바이트 배열이고 System.Guid에 Guid(byte[]) 생성자가 있음에도, $bytes 배열을 개별 파라미터 목록으로 취급해요.
이 흔한 코드 패턴은 의사 메서드 구문(pseudo method syntax)의 한 예다운 사례인데, 항상 의도대로 동작하는 건 아니에요. 이 구문은 실제로 다음과 같이 번역돼요.
PS> [byte[]] $bytes = 1..16
PS> New-Object -TypeName System.Guid -ArgumentList $bytes
New-Object: Cannot find an overload for "Guid" and the argument count: "16".
ArgumentList의 타입이 [Object[]]이기 때문에, 우연히 배열인 단일 인자(어떤 타입이든)는 요소 단위로 바인딩돼요. 해결 방법은 $bytes를 바깥 배열로 한 번 감싸서, PowerShell이 바깥 배열의 내용물과 일치하는 파라미터를 가진 생성자를 찾게 하는 거예요.
PS> [byte[]] $bytes = 1..16
PS> $guid = New-Object -TypeName System.Guid -ArgumentList (, $bytes)
PS> $guid
Guid
----
04030201-0605-0807-090a-0b0c0d0e0f10
감싼 배열의 첫 번째 항목이 바로 우리의 원래 [byte[]] 인스턴스예요. 그 값이 Guid(byte[]) 생성자와 일치하죠.
배열 감싸기 대신, 내장 정적 new() 메서드를 쓰는 방법도 있어요.
PS> [byte[]] $bytes = 1..16
PS> [System.Guid]::new($bytes) # OK
Guid
----
04030201-0605-0807-090a-0b0c0d0e0f10
타입 제한 변수 변환
타입 제한 변수에 값을 할당하면 PowerShell이 그 값을 변수 타입으로 변환하려 시도해요. 주어진 값을 변수 타입으로 변환할 수 있다면 할당이 성공해요.
예를 들어:
PS> [int]$foo = '43'
PS> $foo.GetType().Name
Int32
문자열 '43'을 숫자로 변환할 수 있기 때문에 변환이 동작해요.
연산자 변환
PowerShell은 표현식의 피연산자를 암시적으로 변환해 합리적인 결과를 만들어 낼 수 있어요. 또 일부 연산자는 타입에 특화된 동작을 가지기도 하죠.
숫자 연산
숫자 연산에서는 두 피연산자가 같은 숫자 타입이라도, 결과를 수용하도록 자동 타입 변환이 일어나면서 결과 타입이 달라질 수 있어요.
PS> [int]$a = 1
PS> [int]$b = 2
PS> $result = $a / $b
PS> $result
0.5
PS> $result.GetType().Name
Double
두 피연산자가 모두 정수인데도, 분수 결과를 담기 위해 결과가 Double로 변환돼요. 진짜 정수 나눗셈을 원한다면 [int]::Truncate()나 [Math]::DivRem() 정적 메서드를 쓰면 돼요. 자세한 내용은 Truncate()와 DivRem()을 참고해 주세요.
정수 연산에서 결과가 피연산자 크기를 넘쳐흐를 때, PowerShell은 그 결과가 Int64에 들어갈 수 있더라도 기본적으로 Double을 사용해요.
PS> $result = [int]::MaxValue + 1
PS> $result
2147483648
PS> $result.GetType().Name
Double
결과를 Int64로 만들고 싶다면 결과 타입이나 피연산자를 캐스트하면 돼요.
PS> ([int64]([int]::MaxValue + 1)).GetType().Name
Int64
다만 특정 타입으로 결과를 캐스트할 때는 주의가 필요해요. 예를 들어 결과를 [decimal] 타입으로 캐스트하면 정밀도가 손실될 수 있어요. Int64 최댓값에 1을 더하면 결과가 Double 타입이 되죠. 이 Double을 Decimal로 캐스트하면 결과가 9223372036854780000이 되는데, 정확하지 않아요.
PS> ([int64]::MaxValue + 1).GetType().Name
Double
PS> [decimal]([int64]::MaxValue + 1)
9223372036854780000
이 변환은 정밀도가 15자리로 제한돼요. 자세한 내용은 Decimal(Double) 생성자 문서의 Remarks 섹션을 참고해 주세요.
정밀도 손실을 피하려면 1 리터럴에 D 접미사를 쓰세요. D 접미사를 붙이면 PowerShell이 1D를 더하기 전에 [int64]::MaxValue를 Decimal로 변환해요.
PS> ([int64]::MaxValue + 1D).GetType().Name
Decimal
PS> ([int64]::MaxValue + 1D)
9223372036854775808
숫자 접미사에 대한 자세한 내용은 about_Numeric_Literals를 참고해 주세요.
보통 PowerShell 연산자의 왼쪽(LHS) 피연산자가 연산에 사용되는 데이터 타입을 결정해요. PowerShell은 오른쪽(RHS) 피연산자를 필요한 타입으로 변환(강제)하죠.
PS> 10 - ' 9 '
1
이 예시에서 RHS 피연산자인 문자열 ' 9 '는 뺄셈 연산 전에 암시적으로 정수로 변환돼요. 비교 연산자도 마찬가지예요.
PS> 10 -eq ' 10'
True
PS> 10 -eq '0xa'
True
다만 산술 연산자(+, -, *, /)를 비숫자 피연산자와 함께 쓸 때는 예외가 있어요.
-와 / 연산자를 문자열에 쓰면 PowerShell이 두 피연산자를 문자열에서 숫자로 변환해요.
PS> '10' - '2'
8
PS> '10' / '2'
5
반대로 +와 * 연산자는 문자열에 특화된 의미(연결과 반복)를 가져요.
PS> '10' + '2'
102
PS> '10' * '2'
1010
부울 값을 산술 연산자에 쓰면 PowerShell이 값을 정수로 변환해요. $true는 [int]1이 되고 $false는 [int]0이 되죠.
PS> $false - $true
-1
유일한 예외는 두 부울의 곱셈(*)이에요.
PS> $false * $true
InvalidOperation: The operation '[System.Boolean] * [System.Boolean]' is not
defined.
LHS가 다른 타입일 때 산술 연산자는, 해당 타입이 연산자 오버로딩을 통해 연산자를 직접 정의한 경우에만 성공해요.
비교 연산
-eq, -lt, -gt 같은 비교 연산자는 서로 다른 타입의 피연산자를 비교할 수 있어요. 문자열이 아니고 기본 타입도 아닌 값의 동작은 LHS 타입이 IEquatable, IComparable 같은 인터페이스를 구현하는지에 따라 달라져요.
컬렉션 기반 비교 연산자(-in, -contains)는 일치가 찾아질 때까지 요소별로 -eq 비교를 수행해요. 타입 강제를 이끌어내는 건 컬렉션 피연산자의 각 개별 요소예요.
PS> $true -in 'true', 'false'
True
PS> 'true', 'false' -contains $true
True
두 예시 모두 true를 반환하는데, 'true' -eq $true가 $true가 되기 때문이에요.
자세한 내용은 about_Comparison_Operators를 참고해 주세요.