타입 첫걸음
타입 첫걸음
Scala에서 모든 값은 타입을 가져요. 숫자 값이든 함수든 말이죠. 아래 그림은 그중 일부 타입 계층을 보여줘요.
출처: Scala 3 Book
본문
모든 값에는 타입이 있다
Scala 타입 계층
Any는 모든 타입의 슈퍼타입이라서 top type(최상위 타입)이라고 불러요. equals, hashCode, toString 같은 보편적인 메서드들을 정의하고 있죠.
이 top type인 Any에는 하위 타입으로 Matchable이 있어요. 패턴 매칭을 수행할 수 있는 모든 타입을 표시하는 데 쓰이죠. 이건 parametricity라는 성질을 보장하는 데 중요해요. 여기서 자세히 다루진 않지만, 요약하면 Any 타입의 값에는 패턴 매칭을 할 수 없고 Matchable의 하위 타입인 값에만 할 수 있다는 뜻이에요. Matchable에 대한 더 자세한 내용은 Reference 문서에서 확인할 수 있어요.
Matchable에는 두 가지 중요한 하위 타입이 있어요. 바로 AnyVal과 AnyRef입니다.
AnyVal은 값 타입(value type)을 나타내요. 미리 정의된 값 타입들이 몇 가지 있는데, 모두 non-nullable이에요: Double, Float, Long, Int, Short, Byte, Char, Unit, Boolean. 그중 Unit은 의미 있는 정보를 담지 않는 값 타입이고, 정확히 하나의 인스턴스만 존재하며 ()라고 써요.
AnyRef는 참조 타입(reference type)을 나타내요. 값 타입이 아닌 모든 타입은 참조 타입으로 정의되고, Scala에서 사용자가 정의하는 모든 타입은 AnyRef의 하위 타입이에요. Scala를 Java 런타임 환경에서 쓴다면 AnyRef는 java.lang.Object에 대응합니다.
문장 기반(statement-based) 언어에서는 아무것도 반환하지 않는 메서드에 void를 쓰죠. Scala에서 반환 값이 없는 메서드를 쓸 때도 같은 용도로 Unit을 써요. 예를 들면 이렇게요:
def printIt(a: Any): Unit = println(a)
다음 예제는 문자열, 정수, 문자, 불리언 값, 함수가 모두 Any의 인스턴스이며 다른 모든 객체처럼 취급될 수 있다는 걸 보여줘요.
val list: List[Any] = List(
"a string",
732, // an integer
'c', // a character
'\'', // a character with a backslash escape
true, // a boolean value
() => "an anonymous function returning a string"
)
list.foreach(element => println(element))
이 코드는 List[Any] 타입의 값 list를 정의해요. 리스트는 다양한 타입의 요소들로 초기화되지만, 각 요소는 scala.Any의 인스턴스라서 리스트에 담을 수 있죠. 프로그램의 출력은 이렇게 나와요:
a string
732
c
'
true
<function>
Scala의 "값 타입"
앞서 봤듯이 Scala의 숫자 타입들은 AnyVal을 확장하고 모두 완전한 객체예요. 다음 예제들은 이 숫자 타입들의 변수를 선언하는 방법을 보여줘요.
val b: Byte = 1
val i: Int = 1
val l: Long = 1
val s: Short = 1
val d: Double = 2.0
val f: Float = 3.0
처음 네 예제에서 타입을 명시적으로 지정하지 않으면 숫자 1은 Int로 기본 설정돼요. 그래서 Byte, Long, Short 같은 다른 타입을 원한다면 예제처럼 그 타입을 명시적으로 선언해 줘야 해요. 소수점이 있는 숫자(예: 2.0)는 Double로 기본 설정되므로, Float를 원한다면 마지막 예제처럼 Float로 선언해야 합니다.
Int와 Double이 기본 숫자 타입이기 때문에 보통은 타입을 명시하지 않고 만들게 돼요.
val i = 123 // defaults to Int
val x = 1.0 // defaults to Double
코드에서 숫자에 문자 L, D, F(그리고 소문자 버전)를 붙여서 그 값이 Long, Double, Float임을 지정할 수도 있어요.
val x = 1_000L // val x: Long = 1000
val y = 2.2D // val y: Double = 2.2
val z = -3.3F // val z: Float = -3.3
정수를 16진수 표기법으로도 쓸 수 있어요. (보통 Int지만, L 접미사를 붙여서 Long으로도 지정할 수 있어요.)
val a = 0xACE // val a: Int = 2766
val b = 0xfd_3aL // val b: Long = 64826
Scala는 같은 부동소수점 숫자를 표현하는 여러 가지 방법을 지원해요. 예를 들면:
val q = .25 // val q: Double = 0.25
val r = 2.5e-1 // val r: Double = 0.25
val s = .0025e2F // val s: Float = 0.25
Scala에는 String과 Char 타입도 있는데, 보통은 암시적(타입 생략) 형태로 선언해요.
val s = "Bill"
val c = 'a'
이처럼 문자열은 쌍따옴표로 감싸고(여러 줄 문자열은 삼중 쌍따옴표로), 문자 하나는 홑따옴표로 감싸요. 이 데이터 타입들과 그 범위는 다음과 같아요:
| Data Type | Possible Values |
|---|---|
| Boolean | true 또는 false |
| Byte | 8비트 부호 있는 2의 보수 정수 (-2^7 to 2^7-1, 즉 -128 to 127) |
| Short | 16비트 부호 있는 2의 보수 정수 (-2^15 to 2^15-1, 즉 -32,768 to 32,767) |
| Int | 32비트 2의 보수 정수 (-2^31 to 2^31-1, 즉 -2,147,483,648 to 2,147,483,647) |
| Long | 64비트 2의 보수 정수 (-2^63 to 2^63-1) |
| Float | 32비트 IEEE 754 단정밀도 부동소수점 (1.40129846432481707e-45 to 3.40282346638528860e+38) |
| Double | 64비트 IEEE 754 배정밀도 부동소수점 (4.94065645841246544e-324 to 1.79769313486231570e+308) |
| Char | 16비트 부호 없는 유니코드 문자 (0 to 2^16-1, 즉 0 to 65,535) |
| String | Char의 시퀀스 |
Strings
Scala의 문자열은 Java 문자열과 비슷하지만, (적어도 Java 15 이전과 달리) 삼중 따옴표로 여러 줄 문자열을 아주 쉽게 만들 수 있어요.
val quote = """The essence of Scala:
Fusion of functional and object-oriented
programming in a typed setting."""
이 기본 방식의 한 가지 단점은 첫 줄 이후의 줄들이 들여쓰기되어 보인다는 거예요. 이렇게요:
"The essence of Scala:
Fusion of functional and object-oriented
programming in a typed setting."
공백이 중요할 때는 첫 줄 이후의 모든 줄 앞에 | 기호를 넣고, 문자열 뒤에서 stripMargin 메서드를 호출하면 돼요.
val quote = """The essence of Scala:
|Fusion of functional and object-oriented
|programming in a typed setting.""".stripMargin
이제 모든 줄이 문자열 안에서 왼쪽 정렬돼요.
"The essence of Scala:
Fusion of functional and object-oriented
programming in a typed setting."
Scala 문자열은 강력한 문자열 보간(string interpolation) 메서드도 지원하는데, 그건 다음 장에서 다룰게요.
BigInt와 BigDecimal
정말 큰 숫자가 필요할 때는 BigInt와 BigDecimal 타입을 쓰면 돼요.
val a = BigInt(1_234_567_890_987_654_321L)
val b = BigDecimal(123_456.789)
Double과 Float가 근사치인 소수라면, BigDecimal은 통화 같은 걸 다룰 때 필요한 정밀한 산술에 사용돼요.
BigInt와 BigDecimal의 좋은 점은 숫자 타입에서 쓰던 모든 연산자를 그대로 지원한다는 거예요.
val b = BigInt(1234567890) // scala.math.BigInt = 1234567890
val c = b + b // scala.math.BigInt = 2469135780
val d = b * b // scala.math.BigInt = 1524157875019052100
타입 캐스팅
값 타입은 다음과 같은 방식으로 캐스팅할 수 있어요. 예를 들면:
val b: Byte = 127
val i: Int = b // 127
val face: Char = '☺'
val number: Int = face // 9786
정보의 손실이 없을 때만 타입으로 캐스팅할 수 있어요. 그렇지 않다면 캐스팅을 명시적으로 해줘야 합니다.
val x: Long = 987654321
val y: Float = x.toFloat // 9.8765434E8 (note that `.toFloat` is required because the cast results in precision loss)
val z: Long = y // Error
참조 타입을 하위 타입으로 캐스팅할 수도 있는데, 그건 나중에 투어에서 다룰게요.
Nothing과 null
Nothing은 모든 타입의 하위 타입이라서 bottom type(바닥 타입)이라고 불러요. Nothing 타입을 갖는 값은 존재하지 않습니다. 흔히 던져진 예외, 프로그램 종료, 무한 루프처럼 종료되지 않는 상황을 나타내는 데 쓰여요. 즉 값으로 평가되지 않는 표현식이나 정상적으로 반환되지 않는 메서드의 타입이죠.
Null은 모든 참조 타입(즉 AnyRef의 모든 하위 타입)의 하위 타입이에요. 키워드 리터럴 null로 식별되는 단일 값을 가져요. 현재 null 사용은 나쁜 관행으로 여겨집니다. 대부분 다른 JVM 언어와의 상호운용을 위해 써야 해요. 컴파일러 옵션을 opt-in하면 Null의 상태를 바꿔 그 사용과 관련된 문제점들을 고칠 수 있는데, 이 옵션은 미래 Scala 버전에서 기본값이 될 수도 있어요. 이에 대해 더 자세히 배울 수 있어요.
그동안 Scala 코드에서는 null을 거의 쓰지 말아야 해요. null의 대안들은 이 책의 함수형 프로그래밍 장과 API 문서에서 다룹니다.
더 알아보기
- type hierarchy, top type, bottom type 같은 타입 시스템 개념에 대한 더 깊은 내용은 Reference 문서를 확인해 보세요.
Matchable은 Reference 문서에서 더 자세히 다룹니다.