열린 클래스

열린 클래스 (Open Classes)

클래스에 open 수식어를 붙이면 '이 클래스는 확장을 염두에 두고 설계됐다'는 뜻이 돼요. 기본적으로 Scala 3 클래스는 명시적인 허락 없이는 다른 파일에서 확장하기가 까다로운데, open을 선언하면 그 절차가 훨씬 자연스러워집니다. 이 글에서는 왜 이런 장치가 생겼는지, 언제 쓰는 게 좋은지 살펴볼게요.

출처: Scala 3 Reference

본문

클래스에 open 수식어를 붙이면 그 클래스가 확장을 계획하고 있다는 신호가 돼요. 예를 보겠습니다.

// File Writer.scala
package p

open class Writer[T]:

  /** Sends to stdout, can be overridden */
  def send(x: T) = println(x)

  /** Sends all arguments using `send` */
  def sendAll(xs: T*) = xs.foreach(send)
end Writer

// File EncryptedWriter.scala
package p

class EncryptedWriter[T: Encryptable] extends Writer[T]:
  override def send(x: T) = super.send(encrypt(x))

open 클래스에는 보통 클래스 내부의 메서드 호출 패턴과 재정의할 수 있는 훅(hook)을 설명하는 문서가 함께 딸려 와요. 우리는 이걸 클래스의 **확장 계약(extension contract)**이라고 부릅니다. 클래스와 그것을 쓰는 사용자 사이의 **외부 계약(external contract)**과는 다른 개념이에요.

open으로 선언되지 않은 클래스도 여전히 확장할 수는 있는데, 다음 두 조건 중 적어도 하나를 만족해야 해요.

  • 확장하는 클래스가 확장 대상 클래스와 같은 소스 파일에 있을 때. 이 경우 확장은 보통 내부 구현 사항이에요.
  • 확장하는 클래스에 adhocExtensions 언어 기능이 켜져 있을 때. 보통 확장 소스 파일의 import 절로 켜요:
import scala.language.adhocExtensions

대안으로 컴파일러 옵션 -language:adhocExtensions로도 켤 수 있어요. 기능이 켜지지 않았다면 컴파일러가 "feature" 경고를 냅니다. 예를 들어 Writer 클래스의 open 수식어를 빼면, EncryptedWriter를 컴파일할 때 이런 경고가 나와요:

-- Feature Warning: EncryptedWriter.scala:6:14 ----
  |class EncryptedWriter[T: Encryptable] extends Writer[T]
  |                                              ^
  |Unless class Writer is declared 'open', its extension
  | in a separate file should be enabled
  |by adding the import clause 'import scala.language.adhocExtensions'
  |or by setting the compiler option -language:adhocExtensions.

동기 (Motivation)

클래스를 작성할 때 확장 가능성에 대한 기대는 세 가지로 나뉘어요:

  • 클래스가 확장을 의도하고 있다. 이 경우 그 클래스에는 신중하게 정리되고 문서화된 확장 계약이 있다고 기대해야 해요.
  • 클래스의 확장이 금지되어 있다. 예를 들어 정확성이나 보안을 보장하기 위해서죠.
  • 어느 쪽으로도 확실한 결정을 내리지 않았다. 클래스가 처음부터 확장을 염두에 둔 건 아니지만, 남들이 임시방편(ad-hoc)으로 확장하는 게 유용하다면 그렇게 하도록 두는 경우에요. 다만 이 경우 확장하는 쪽은 스스로 책임져야 해요. 문서화된 확장 계약이 없고, 미래 버전에서 내부 호출 패턴이 재배치되는 등의 이유로 확장이 깨질 수도 있거든요.

세 경우는 open으로 (1)을, final로 (2)를, 수식어 없음으로 (3)을 명확히 구분해서 표현해요.

코드베이스에서 임시방편 확장은 되도록 피하는 게 좋아요. 그런 패턴은 진화시키기 어려운 깨지기 쉬운 시스템으로 이어지기 쉽거든요. 그래도 시험에서 클래스를 mock 하거나, 라이브러리 클래스에 임시 패치를 가하는 등 유용한 상황도 분명히 있어요. 그래서 임시방편 확장은 허용하되, 언어 기능 import를 통한 명시적 opt-in이 있을 때만 허용하는 거예요.

세부 사항 (Details)

  • open은 soft 수식어예요. 수식어 위치에 있지 않으면 일반 식별자로 취급됩니다.
  • open 클래스는 final일 수도, sealed일 수도 없어요.
  • trait나 추상 클래스는 항상 open이므로, 이들에게 open은 중복되는 표시예요.

sealed와의 관계 (Relationship with sealed)

추상도 아니고 open도 아닌 클래스는 sealed 클래스와 비슷해요. 여전히 확장할 수 있지만 같은 소스 파일에서만 확장할 수 있죠. 차이는 다른 소스 파일에서 확장을 시도했을 때 드러나요. sealed 클래스라면 그 자체가 오류이지만, 단순한 non-open 클래스라면 adhocExtensions 기능이 켜져 있을 때는 여전히 허용되고, 그렇지 않으면 경고가 나와요.

마이그레이션 (Migration)

open은 Scala 3에 새로 추가된 수식어예요. Scala 2.13과 Scala 3.0 사이의 경고 없는 크로스 컴파일을 허용하기 위해, ad-hoc 확장에 대한 feature 경고는 -source future에서만 발생해요. Scala 3.4부터는 기본으로 발생하게 됩니다.