가변성

스칼라는 제네릭 클래스의 타입 파라미터에 관한 가변성 어노테이션을 지원한다. 자바 5(다른 이름은 JDK 1.5)에선 추상화된 클래스가 사용될 때 클라이언트가 가변성 어노테이션을 결정하지만, 반면에 스칼라는 추상화된 클래스를 정의할 때 가변성 어노테이션을 추가할 수 있다.

제네릭 클래스에 관한 페이지에선 변경 가능한 스택의 예제를 살펴보면서, 클래스 Stack[T]에서 정의한 타입은 타입 파라미터의 서브타입이 불변자여야 함을 설명했었다. 이는 추상화된 클래스의 재사용을 제한할 수 있다. 지금부턴 이런 제약이 없는 함수형(즉, 변경이 불가능한) 스택의 구현을 알아본다. 이 구현은 다형성 메소드, 하위 타입 경계, 순가변 타입 파라미터 어노테이션 등의 중요 개념을 조합한 좀 더 어려운 예제임을 알아두자. 또한 내부 클래스를 사용해 명시적인 연결 없이도 스택의 항목을 서로 묶을 수 있도록 만들었다.

class Stack[+T] {
  def push[S >: T](elem: S): Stack[S] = new Stack[S] {
    override def top: S = elem
    override def pop: Stack[S] = Stack.this
    override def toString: String =
      elem.toString + " " + Stack.this.toString
  }
  def top: T = sys.error("no element on stack")
  def pop: Stack[T] = sys.error("no element on stack")
  override def toString: String = ""
}

object VariancesTest extends App {
  var s: Stack[Any] = new Stack().push("hello")
  s = s.push(new Object())
  s = s.push(7)
  println(s)
}

어노테이션 +T는 타입 순가변 위치에서만 사용할 수 있는 타입 T를 선언한다. 이와 유사하게, -T는 역가변 위치에서만 사용할 수 있는 T를 선언한다. 순가변 타입 파라미터의 경우, 타입 파라미터의 정의에 따라 서브타입과 순가변 관계를 형성한다. 즉, TS의 서브타입이라면 이 예제의 Stack[T]Stack[S]의 서브타입이다. -로 표시된 타입 파라미터 사이에는 이와는 반대의 관계가 맺어진다.

스택의 예제에서 메소드 push를 정의하기 위해선 역가변 위치에 순가변 타입 파라미터 T를 사용해야만 한다. 스택의 서브타입이 순가변적여야 하기 때문에, 메소드 push의 파라미터 타입을 추상화하는 기법을 사용했다. 즉, 스택 항목의 타입인 T를 사용해, Tpush의 타입 변수 하위 경계로 설정된 다형성 메소드를 만들었다. 이는 T 선언에 따른 가변성이 순가변 타입 파라미터와 같도록 해준다. 이제 스택은 순가변적이지만, 지금까지 알아본 방법에 따르면 정수 스택에 문자열을 푸시하는 것과 같은 동작이 가능해진다. 이런 동작은 Stack[Any]라는 타입의 스택을 만드는데, 결국 정수 스택이 결과로 반환되길 기다리고 있는 상황에서 이 결과는 오류를 발생시킨다. 오류가 발생하지 않더라도, 항목의 타입이 더 일반화된 스택이 반환될 뿐이다.

윤창석, 이한욱 옮김