구조적 타입
구조적 타입 (Structural Types)
이 문서 페이지는 Scala 3 전용이에요. Scala 2에는 없는 새로운 개념을 다룰 수 있어요. 달리 명시하지 않는 한, 이 페이지의 모든 코드 예제는 Scala 3을 사용한다고 가정합니다.
Scala 2에는 자바 리플렉션 기반의 더 약한 형태의 구조적 타입이 있었는데, import scala.language.reflectiveCalls로 사용했어요.
도입 (Introduction)
데이터베이스 접근을 모델링하는 것 같은 일부 사용 사례는 동적 타입 언어에서는 자연스러운데, 정적 타입 언어에서는 좀 더 어색해요. 동적 타입 언어에서는 행(row)을 레코드나 객체로 모델링하고, row.columnName처럼 간단한 점 표기법으로 항목을 선택하는 게 자연스럽죠.
정적 타입 언어에서 같은 경험을 얻으려면 데이터베이스 조작에서 나올 수 있는 모든 행마다 클래스를 정의해야 해요. 조인(join)과 프로젝션(projection)에서 생기는 행까지 포함해서 말이죠. 그리고 행과 그 행을 표현하는 클래스 사이의 매핑 방식을 세팅해야 하구요. 이렇게 하려면 상당한 양의 보일러플레이트 코드가 필요한데, 그래서 개발자들은 정적 타입의 장점을 포기하고 열 이름을 문자열로 표현해 다른 연산자에 넘기는 더 단순한 방식(row.select("columnName") 같은)을 선택하게 돼요. 하지만 이 방식은 정적 타입의 장점을 놓치면서도, 여전히 동적 타입 버전만큼 자연스럽지 않아요.
구조적 타입(structural type)은 동적 컨텍스트에서 단순한 점 표기법을 지원하면서도 정적 타입의 장점을 잃지 않으려는 상황에서 도움이 돼요. 개발자가 점 표기법을 쓰면서 필드와 메서드를 어떻게 해석할지 구성할 수 있게 해 주죠.
예시 (Example)
구조적 타입 Person의 예를 볼게요.
class Record(elems: (String, Any)*) extends Selectable:
private val fields = elems.toMap
def selectDynamic(name: String): Any = fields(name)
type Person = Record {
val name: String
val age: Int
}
Person 타입은 부모 타입 Record에 name, age 필드를 정의하는 리파인먼트(refinement) 를 더해요. name과 age가 부모 타입에는 정의되어 있지 않으므로, 이 리파인먼트가 구조적(structural) 이라고 말해요. 하지만 그럼에도 name과 age는 Person 클래스의 멤버로 존재합니다. 예를 들어 다음 프로그램은 “Emma is 42 years old.”라는 문자열을 출력해요.
val person = Record(
"name" -> "Emma",
"age" -> 42
).asInstanceOf[Person]
println(s"${person.name} is ${person.age} years old.")
이 예시에서 부모 타입 Record는 elems 인자에 임의의 레코드를 표현할 수 있는 일반(제네릭) 클래스예요. 이 인자는 String 타입의 레이블과 Any 타입의 값 쌍으로 이뤄진 시퀀스죠. Person을 Record로 만들 때는, 레코드가 올바른 타입의 올바른 필드를 정의했다는 걸 타입 캐스트로 단언(assert)해야 해요. Record 자체는 너무 약하게 타입되어 있어서, 사용자의 도움 없이는 컴파일러가 그걸 알 수 없거든요. 실제로는 구조적 타입과 그 밑의 일반 표현 사이의 연결을 데이터베이스 계층이 맺어 주는 경우가 대부분이라, 최종 사용자가 신경 쓸 일은 아니에요.
Record는 마커 트레이트 scala.Selectable를 확장하고, 필드 이름을 그 값에 매핑하는 메서드 selectDynamic을 정의해요. 구조적 타입 멤버를 선택하는 건 이 메서드를 호출하는 방식으로 이뤄집니다. person.name과 person.age 선택은 Scala 컴파일러에 의해 이렇게 변환돼요.
person.selectDynamic("name").asInstanceOf[String]
person.selectDynamic("age").asInstanceOf[Int]
두 번째 예시 (A second example)
방금 본 내용을 확실히 익히기 위해, 데이터베이스에서 읽어올 책을 표현하는 Book이라는 구조적 타입도 살펴볼게요.
type Book = Record {
val title: String
val author: String
val year: Int
val rating: Double
}
Person과 마찬가지로, Book 인스턴스는 이렇게 만들어요.
val book = Record(
"title" -> "The Catcher in the Rye",
"author" -> "J. D. Salinger",
"year" -> 1951,
"rating" -> 4.5
).asInstanceOf[Book]
Selectable 클래스 (Selectable class)
selectDynamic 말고도, Selectable 클래스가 applyDynamic 메서드를 정의하기도 해요. 이 메서드는 구조적 멤버의 함수 호출을 변환하는 데 사용됩니다. 그래서 a가 Selectable의 인스턴스라면, a.f(b, c) 같은 구조적 호출은 이렇게 변환돼요.
a.applyDynamic("f")(b, c)