보편적 apply 메서드

보편적 apply 메서드 (Universal Apply Methods)

Scala의 case 클래스는 apply 메서드를 생성해 줘서, case 클래스의 값을 간단한 함수 적용으로 만들 수 있었어요. new를 쓸 필요가 없었죠.

Scala 3은 이 방식을 **모든 구체 클래스(concrete class)**로 일반화해요. 예시를 볼게요:

class StringBuilder(s: String):
  def this() = this("")

StringBuilder("abc")  // old: new StringBuilder("abc")
StringBuilder()       // old: new StringBuilder()

출처: Scala 3 Reference

본문

이게 동작하는 이유는 클래스와 함께 두 개의 apply 메서드를 가진 컴패니언 객체(companion object)가 생성되기 때문이에요. 그 객체는 다음과 같이 생겼죠:

object StringBuilder:
  inline def apply(s: String): StringBuilder = new StringBuilder(s)
  inline def apply(): StringBuilder = new StringBuilder()

이 합성 객체 StringBuilder와 그 apply 메서드를 **생성자 프록시(constructor proxy)**라고 불러요. 생성자 프록시는 Java 클래스와 Scala 2에서 온 클래스에 대해서도 생성돼요. 정확한 규칙은 다음과 같아요.

  • 구체 클래스 C에 대해 생성자 프록시 컴패니언 객체 object C가 만들어져요. 단, 클래스에 이미 컴패니언이 없고, C가 정의된 스코프에 C라는 이름을 가진 다른 값이나 메서드가 정의되거나 상속되지 않은 경우에만요.
  • 생성자 프록시 apply 메서드는 구체 클래스에 대해 다음과 같은 경우에 생성돼요:
    • 클래스에 컴패니언 객체가 있고(1단계에서 생성됐을 수도 있죠),
    • 그 컴패니언 객체가 이미 apply라는 이름의 멤버를 정의하지 않은 경우.
  • 각 생성된 apply 메서드는 클래스의 생성자 하나로 전달돼요(forward). 생성자와 같은 타입·값 파라미터와 같은 접근 제한을 가져요. 클래스가 protected라면 컴패니언 객체도 protected여야 하거나, apply 메서드가 protected로 만들어져요.

생성자 프록시 컴패니언은 그 자체로 값으로 사용될 수 없어요. 프록시 컴패니언 객체는 apply로 선택되거나(또는 인자에 적용되는데, 이 경우 apply가 암시적으로 삽입돼요) 해야 해요.

생성자 프록시는 또한 일반 정의를 가리는(shadow) 것이 허용되지 않아요. 즉, 다음 상황에서는 **모호성(ambiguity)**이 보고돼요:

  • 어떤 식별자가 생성자 프록시로 해석되는데,
  • 같은 식별자가 다른 어떤 스코프에서도 정의되거나 import되고,
  • 그 다른 참조가 (비어 있을 수 있는) 파라미터 리스트에 적용될 수 있을 때. 즉, 메서드를 가리키거나, 멤버로 apply 메서드를 가진 값을 가리킬 때.

동기 (Motivation)

new를 생략하는 것은 구현 세부사항을 숨기고 코드를 더 읽기 좋게 만들어요. 새로운 규칙이 필요하긴 하지만, 언어의 규칙성이 더 높아진 것처럼 느껴질 거예요. case 클래스는 이미 함수 호출 생성 문법을 제공하니까요(그리고 종종 바로 그 이유 때문에 정의되기도 하죠).