불투명 타입

불투명 타입 (Opaque Types)

**불투명 타입 별칭(opaque type alias)**은 오버헤드 없이 타입 추상화를 제공해요. Scala 2에서는 값 클래스(value class)로 비슷한 결과를 얻을 수 있었어요.

출처: Scala 3 Book

본문

추상화 오버헤드

숫자를 그 로그(logarithm)로 표현해서 산술 연산을 제공하는 모듈을 정의하고 싶다고 가정해 볼게요. 이 방식은 관련된 숫자 값이 매우 크거나 0에 가까울 때 정밀도를 높이는 데 유용할 수 있어요. "일반적인" double 값과 로그로 저장된 숫자를 구분하는 게 중요하기 때문에, Logarithm 클래스를 도입해요.

class Logarithm(protected val underlying: Double):
  def toDouble: Double = math.exp(underlying)
  def + (that: Logarithm): Logarithm =
    // here we use the apply method on the companion
    Logarithm(this.toDouble + that.toDouble)
  def * (that: Logarithm): Logarithm =
    new Logarithm(this.underlying + that.underlying)

object Logarithm:
  def apply(d: Double): Logarithm = new Logarithm(math.log(d))

동반 객체(companion object)의 apply 메서드를 쓰면 Logarithm 타입의 값을 만들 수 있고, 아래처럼 사용할 수 있어요.

val l2 = Logarithm(2.0)
val l3 = Logarithm(3.0)
println((l2 * l3).toDouble) // prints 6.0
println((l2 + l3).toDouble) // prints 4.999...

Logarithm 클래스는 이런 특수한 로그 형태로 저장된 Double 값에 멋진 추상화를 제공하지만, 심각한 성능 오버헤드를 부과해요. 모든 수학 연산마다 기저 값을 꺼내서 다시 Logarithm의 새 인스턴스로 감싸야 하기 때문이에요.

모듈 추상화

같은 라이브러리를 구현하는 또 다른 방법을 생각해 봐요. 이번에는 Logarithm을 클래스가 아니라 **타입 별칭(type alias)**으로 정의해요. 먼저 모듈의 추상 인터페이스를 정의해요.

trait Logarithms:

  type Logarithm

  // operations on Logarithm
  def add(x: Logarithm, y: Logarithm): Logarithm
  def mul(x: Logarithm, y: Logarithm): Logarithm

  // functions to convert between Double and Logarithm
  def make(d: Double): Logarithm
  def extract(x: Logarithm): Double

  // extension methods to use `add` and `mul` as "methods" on Logarithm
  extension (x: Logarithm)
    def toDouble: Double = extract(x)
    def + (y: Logarithm): Logarithm = add(x, y)
    def * (y: Logarithm): Logarithm = mul(x, y)

이제 type LogarithmDouble과 같다고 선언해서 이 추상 인터페이스를 구현해요.

object LogarithmsImpl extends Logarithms:

  type Logarithm = Double

  // operations on Logarithm
  def add(x: Logarithm, y: Logarithm): Logarithm = make(x.toDouble + y.toDouble)
  def mul(x: Logarithm, y: Logarithm): Logarithm = x + y

  // functions to convert between Double and Logarithm
  def make(d: Double): Logarithm = math.log(d)
  def extract(x: Logarithm): Double = math.exp(x)

LogarithmsImpl 구현 안에서는 Logarithm = Double이라는 등식 덕분에 여러 메서드를 구현할 수 있어요.

잘 새는 추상화 (Leaky Abstractions)

하지만 이 추상화는 약간 "새는(leaky)" 편이에요. 항상 추상 인터페이스 Logarithms에 대해서만 프로그래밍하고, LogarithmsImpl을 직접 쓰면 안 된다는 점을 지켜야 해요. LogarithmsImpl을 직접 쓰면 Logarithm = Double이라는 등식이 사용자에게 보이게 돼서, 사용자가 로그 형태의 double이 기대되는 자리에 Double을 실수로 쓰게 될 수 있어요. 예를 들어:

import LogarithmsImpl.*
val l: Logarithm = make(1.0)
val d: Double = l // type checks AND leaks the equality!

모듈을 추상 인터페이스와 구현으로 나눠야 하는 건 유용할 수 있지만, Logarithm의 구현 세부 사항을 숨기기만 하려는 데는 꽤 많은 노력이 들어가요. 추상 모듈 Logarithms에 대해 프로그래밍하는 건 매우 지루할 수 있고, 아래 예시처럼 경로 의존 타입(path-dependent type) 같은 고급 기능을 자주 요구해요.

def someComputation(L: Logarithms)(init: L.Logarithm): L.Logarithm = ...

박싱 오버헤드 (Boxing Overhead)

type Logarithm 같은 타입 추상화는 그 바운드로 소거(erase)돼요 (우리 경우에는 Any). 다시 말해 Double 값을 수동으로 감싸고 풀지 않아도 되지만, 원시 타입 Double을 박싱하는 것과 관련된 약간의 박싱 오버헤드는 여전히 존재해요.

불투명 타입 (Opaque Types)

Logarithms 컴포넌트를 추상 부분과 구체 구현으로 수동으로 나누는 대신, Scala 3의 불투명 타입을 그냥 쓰면 비슷한 효과를 얻을 수 있어요.

object Logarithms:
//vvvvvv this is the important difference!
  opaque type Logarithm = Double

  object Logarithm:
    def apply(d: Double): Logarithm = math.log(d)

  extension (x: Logarithm)
    def toDouble: Double = math.exp(x)
    def + (y: Logarithm): Logarithm = Logarithm(math.exp(x) + math.exp(y))
    def * (y: Logarithm): Logarithm = x + y

LogarithmDouble과 같다는 사실은 Logarithm이 정의된 범위, 즉 위 예시에서 object Logarithms에 해당하는 곳에서만 알려져요. Logarithm = Double이라는 타입 등식은 메서드(예: *, toDouble)를 구현하는 데 쓸 수 있어요. 그러나 모듈 바깥에서는 Logarithm 타입이 완전히 캡슐화되어, 즉 "불투명(opaque)"해요. Logarithm 사용자는 Logarithm이 실제로 Double로 구현됐다는 사실을 알아낼 수 없어요.

import Logarithms.*
val log2 = Logarithm(2.0)
val log3 = Logarithm(3.0)
println((log2 * log3).toDouble) // prints 6.0
println((log2 + log3).toDouble) // prints 4.999...

val d: Double = log2 // ERROR: Found Logarithm required Double

Logarithm에 대해 추상화했지만, 그 추상화는 공짜로 얻을 수 있어요. 구현이 하나뿐이기 때문에 런타임에 Double 같은 원시 타입에 대한 박싱 오버헤드가 없기 때문이에요.

불투명 타입 요약

불투명 타입은 성능 오버헤드를 부과하지 않으면서 구현 세부 사항에 대한 건전한 추상화를 제공해요. 위에서 보여줬듯이 불투명 타입은 사용하기 편리하고, 확장 메서드(Extension Methods) 기능과 잘 통합돼요.

더 알아보기 (Learn more)