구조적 타입

구조적 타입 (Structural Types)

이 문서 페이지는 Scala 3 전용이에요. Scala 2에는 없는 새로운 개념을 다룰 수 있어요. 달리 명시하지 않는 한, 이 페이지의 모든 코드 예제는 Scala 3을 사용한다고 가정합니다.

Scala 2에는 자바 리플렉션 기반의 더 약한 형태의 구조적 타입이 있었는데, import scala.language.reflectiveCalls로 사용했어요.

출처: Scala 3 Book — Structural Types

도입 (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 타입은 부모 타입 Recordname, age 필드를 정의하는 리파인먼트(refinement) 를 더해요. nameage가 부모 타입에는 정의되어 있지 않으므로, 이 리파인먼트가 구조적(structural) 이라고 말해요. 하지만 그럼에도 nameagePerson 클래스의 멤버로 존재합니다. 예를 들어 다음 프로그램은 “Emma is 42 years old.”라는 문자열을 출력해요.

val person = Record(
  "name" -> "Emma",
  "age" -> 42
).asInstanceOf[Person]

println(s"${person.name} is ${person.age} years old.")

이 예시에서 부모 타입 Recordelems 인자에 임의의 레코드를 표현할 수 있는 일반(제네릭) 클래스예요. 이 인자는 String 타입의 레이블과 Any 타입의 값 쌍으로 이뤄진 시퀀스죠. PersonRecord로 만들 때는, 레코드가 올바른 타입의 올바른 필드를 정의했다는 걸 타입 캐스트로 단언(assert)해야 해요. Record 자체는 너무 약하게 타입되어 있어서, 사용자의 도움 없이는 컴파일러가 그걸 알 수 없거든요. 실제로는 구조적 타입과 그 밑의 일반 표현 사이의 연결을 데이터베이스 계층이 맺어 주는 경우가 대부분이라, 최종 사용자가 신경 쓸 일은 아니에요.

Record는 마커 트레이트 scala.Selectable를 확장하고, 필드 이름을 그 값에 매핑하는 메서드 selectDynamic을 정의해요. 구조적 타입 멤버를 선택하는 건 이 메서드를 호출하는 방식으로 이뤄집니다. person.nameperson.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 메서드를 정의하기도 해요. 이 메서드는 구조적 멤버의 함수 호출을 변환하는 데 사용됩니다. 그래서 aSelectable의 인스턴스라면, a.f(b, c) 같은 구조적 호출은 이렇게 변환돼요.

a.applyDynamic("f")(b, c)

더 알아보기 (Learn more)