타입 클래스

타입 클래스 (Type Classes)

객체지향에서는 어떤 동작을 여러 타입에 공통으로 적용하고 싶을 때 주로 상속(서브타이핑)을 떠올려요. 그런데 상속을 쓸 수 없는 타입, 예를 들어 내가 소유하지 않은 라이브러리의 타입에 새 동작을 더해야 한다면 어떨까요? 그럴 때 쓰는 도구가 바로 타입 클래스(type class)예요.

출처: Scala 3 Book

본문

타입 클래스는 추상적이고 파라미터화된 타입으로, 서브타이핑을 쓰지 않고도 어떤 폐쇄된(closed) 데이터 타입에 새 동작을 추가할 수 있게 해 줘요. Java에서 왔다면, 타입 클래스를 java.util.Comparator[T] 같은 것이라고 생각하면 비슷해요.

Oliveira 등의 논문 "Type Classes as Objects and Implicits"(2010)가 스칼라에서 타입 클래스의 기본 아이디어를 다루고 있어요. 이 논문은 예전 버전의 스칼라를 쓰지만, 그 아이디어는 오늘날까지도 유효해요.

타입 클래스는 여러 상황에서 유용해요. 예를 들면 이런 경우죠.

  • 표준 라이브러리나 서드파티 라이브러리처럼 내가 소유하지 않은 타입이 어떤 동작을 따르는지를 표현할 때.
  • 그런 타입들 사이에 서브타이핑 관계 없이 여러 타입에 그 동작을 표현할 때.

타입 클래스는 하나 이상의 파라미터를 가진 트레이트(trait)예요. 그 구현체는 Scala 3에서는 given 인스턴스로, Scala 2에서는 implicit 값으로 제공돼요.

예제

예를 들어 Show는 Haskell에서 잘 알려진 타입 클래스예요. 아래 코드는 그걸 스칼라로 구현하는 한 가지 방법을 보여줘요. 만약 스칼라 클래스에 toString 메서드가 없다고 상상해 보세요. 그러면 커스텀 문자열로 변환하고 싶은 아무 타입에나 그 동작을 추가하기 위해 Show 타입 클래스를 정의할 수 있어요.

타입 클래스 정의

타입 클래스를 만드는 첫 단계는 하나 이상의 추상 메서드를 가진 파라미터화된 트레이트를 선언하는 거예요. Showableshow라는 메서드 하나만 가지므로, 이렇게 작성해요.

Scala 2 버전

// a type class
trait Showable[A] {
  def show(a: A): String
}

Scala 3 버전

// a type class
trait Showable[A]:
  extension (a: A) def show: String

이 방식이 우리가 흔히 쓰던 객체지향 방식과 얼마나 가까운지 눈여겨보세요. 객체지향에서는 보통 Show 트레이트를 이렇게 정의하거든요.

Scala 2 버전

// a trait
trait Show {
  def show: String
}

Scala 3 버전

// a trait
trait Show:
  def show: String

여기서 몇 가지 중요한 점을 짚어 볼게요.

  • Showable 같은 타입 클래스는 **타입 파라미터 A**를 받아서, show의 구현을 어떤 타입에 대해 제공하는지 말해요. 반면 Show 같은 고전적인 트레이트는 그렇지 않아요.
  • 특정 타입 Ashow 기능을 추가하려면, 고전 트레이트는 AShow상속해야 해요. 반면 타입 클래스는 Showable[A]구현을 요구해요.
  • Scala 3에서 Showable.showShow의 호출 문법과 동일하게 쓰기 위해, Showable.show확장 메서드(extension method)로 정의했어요.

구체적인 인스턴스 구현하기

다음 단계는 애플리케이션에서 Showable이 동작할 클래스들을 정하고, 그 동작을 구현하는 거예요. 예를 들어 이런 Person 클래스에 Showable을 구현한다고 해 볼게요.

case class Person(firstName: String, lastName: String)

그러면 타입 Person에 대한 Showable[Person]의 표준(canonical) 값, 즉 인스턴스를 하나 정의하면 돼요. 다음 코드 예제가 그걸 보여줘요.

Scala 2 버전

implicit val showablePerson: Showable[Person] = new Showable[Person] {
  def show(p: Person): String =
    s"${p.firstName} ${p.lastName}"
}

Scala 3 버전

given Showable[Person] with
  extension (p: Person) def show: String =
    s"${p.firstName} ${p.lastName}"

타입 클래스 사용하기

이제 이 타입 클래스를 이렇게 사용할 수 있어요.

Scala 2 버전

val person = Person("John", "Doe")
println(showablePerson.show(person))

참고로 실제로는 타입 클래스를 Person처럼 타입이 알려진 값보다는, 다음 절에서 보여주는 것처럼 타입이 알려지지 않은 값과 함께 쓰는 경우가 일반적이에요.

Scala 3 버전 (확장 메서드 문법)

val person = Person("John", "Doe")
println(person.show)

다시 말해, 스칼라의 모든 클래스에 toString 메서드가 없다면, 이 기법을 사용해 문자열로 변환하고 싶은 아무 클래스에나 Showable 동작을 추가할 수 있어요.

타입 클래스를 사용하는 메서드 작성하기

상속과 마찬가지로, Showable을 타입 파라미터로 사용하는 메서드를 정의할 수 있어요.

Scala 2 버전

def showAll[A](as: List[A])(implicit showable: Showable[A]): Unit =
  as.foreach(a => println(showable.show(a)))

showAll(List(Person("Jane"), Person("Mary")))

Scala 3 버전

def showAll[A: Showable](as: List[A]): Unit =
  as.foreach(a => println(a.show))

showAll(List(Person("Jane"), Person("Mary")))

메서드가 여러 개인 타입 클래스

여러 메서드를 가진 타입 클래스를 만들고 싶다면, 초기 문법은 이렇게 생겼어요.

Scala 2 버전

trait HasLegs[A] {
  def walk(a: A): Unit
  def run(a: A): Unit
}

Scala 3 버전

trait HasLegs[A]:
  extension (a: A)
    def walk(): Unit
    def run(): Unit

실제 사례

Scala 3에서 타입 클래스가 실제로 어떻게 쓰이는지 보려면, 다중 우주 동등성(Multiversal Equality) 절의 CanEqual 설명을 참고하면 돼요.

더 알아보기 (Learn more)