암시적 변환

암시적 변환 (Implicit Conversions)

한 타입의 값을 마치 다른 타입인 것처럼 넘겨주게 해서, 반복되는 보일러플레이트 코드를 줄여주는 강력한 기능이에요. 강력한 만큼 함부로 쓰면 곤란한 경우도 있어서, 본문 뒤쪽에서 주의할 점까지 함께 살펴볼게요.

Scala 2에서는 닫혀 있는(closed) 클래스에 멤버를 추가하는 용도로도 암시적 변환이 쓰였어요(Implicit Classes 문서 참고). Scala 3에서는 그런 용도는 암시적 변환 대신 확장 메서드(extension method)로 다루는 걸 권장해요. 다만 표준 라이브러리는 역사적인 이유로 여전히 암시적 변환에 의존하고 있고요.

출처: Scala 3 Book

본문

예시 (Example)

Long 타입 파라미터를 받는 findUserById 메서드를 생각해 볼게요.

def findUserById(id: Long): Option[User]

User 타입의 정의는 예시에서 중요하지 않으니 간결함을 위해 생략할게요.

Scala에서는 findUserById 메서드를 예상 타입 Long 대신 Int 타입의 인자로 호출할 수 있어요. 인자가 암시적으로 Long 타입으로 변환되기 때문이죠.

val id: Int = 42
findUserById(id) // OK

이 코드는 "type mismatch: expected Long, found Int" 같은 컴파일 오류 없이 잘 컴파일돼요. 인자 idLong 타입 값으로 바꿔 주는 암시적 변환이 있기 때문이에요.

자세한 설명 (Detailed Explanation)

암시적 변환을 어떻게 정의하고 사용하는지 자세히 살펴볼게요.

암시적 변환 정의하기 (Defining an Implicit Conversion)

Scala 3에서 타입 S에서 타입 T로의 암시적 변환은 scala.Conversion[S, T] 타입의 given 인스턴스로 정의해요. Scala 2와의 호환성을 위해 암시적 메서드로도 정의할 수 있지만, Scala 3에서는 아래 방식을 권장해요.

예를 들어 Int에서 Long으로 가는 암시적 변환은 이렇게 정의해요.

given int2long: Conversion[Int, Long] with
  def apply(x: Int): Long = x.toLong

다른 given 정의와 마찬가지로 암시적 변환도 익명으로 쓸 수 있어요.

given Conversion[Int, Long] with
  def apply(x: Int): Long = x.toLong

별명(alias)을 사용하면 더 간결하게 표현할 수 있어요.

given Conversion[Int, Long] = (x: Int) => x.toLong

(Scala 2에서는 Int에서 Long으로의 암시적 변환이 import scala.language.implicitConversions 아래에 암시적 메서드로 정의되었어요. 자세한 내용은 "암시적 변환의 힘을 경계하자" 절을 참고하세요.)

암시적 변환 사용하기 (Using an Implicit Conversion)

암시적 변환은 두 가지 상황에서 적용돼요.

  1. 표현식 e의 타입이 S인데, S가 표현식의 예상 타입 T에 맞지 않을 때
  2. 타입 S의 표현식에 대한 선택(selection) e.m에서, 선택자 mS의 멤버를 가리키지 않을 때(Scala 2 스타일의 확장 메서드를 지원하기 위함)

첫 번째 경우에는 e에 적용 가능하고 결과 타입이 T에 맞는 변환 c를 찾아요. 위 예시에서 타입 Int의 인자 idfindUserById에 넘기면, 암시적 변환 int2long(id)이 삽입되는 거예요.

두 번째 경우에는 e에 적용 가능하고 결과에 m이라는 멤버가 포함된 변환 c를 찾아요. 예를 들어 두 문자열을 비교하는 "foo" < "bar" 같은 경우가 있어요. 여기서 String< 멤버가 없으므로, 암시적 변환 Predef.augmentString("foo") < "bar"이 삽입돼요. (scala.Predef는 모든 Scala 프로그램에 자동으로 임포트돼요.)

암시적 변환은 어떻게 스코프 안으로 들어올까? (How Are Implicit Conversions Brought Into Scope?)

컴파일러가 적용 가능한 변환을 검색할 때는 이 순서로 살펴봐요.

  • 먼저 현재의 어휘 스코프(lexical scope)를 봐요.
    • 현재 스코프나 바깥 스코프에 정의된 암시적 변환
    • 임포트된 암시적 변환
    • 와일드카드 임포트로 들여온 변환 (Scala 2 전용)
  • 그다음에 인자 타입 S나 예상 타입 T에 연관된 컴패니언 객체(companion object)를 봐요. 타입 X에 연관된 컴패니언 객체는 다음을 포함해요.
    • 컴패니언 객체 X 자체
    • X가 상속한 타입들에 연관된 컴패니언 객체
    • X의 타입 인자에 연관된 컴패니언 객체
    • X가 내부 클래스라면, 그걸 감싸고 있는 바깥 객체들

예를 들어 객체 Conversions 안에 정의된 fromStringToUser 암시적 변환을 생각해 볼게요.

object Conversions:
  given fromStringToUser: Conversion[String, User] = (name: String) => User(name)

그러면 아래 임포트들이 동등하게 이 변환을 스코프 안으로 가져와요.

import Conversions.fromStringToUser
// 또는
import Conversions.given
// 또는
import Conversions.{given Conversion[String, User]}

여기서 주의할 점이 있어요. Scala 3에서는 와일드카드 임포트(즉 import Conversions.*)가 given 정의를 임포트하지 않아요.

도입부 예시에서 Int에서 Long으로의 변환이 임포트 없이 동작했던 이유는, 그 변환이 Int 타입의 컴패니언 객체인 Int 객체 안에 정의되어 있었기 때문이에요.

더 읽을거리: Where does Scala look for implicits? (Stackoverflow)

암시적 변환의 힘을 경계하자 (Beware the Power of Implicit Conversions)

암시적 변환은 가리지 않고 남용하면 함정이 될 수 있어서, 컴파일러는 두 가지 상황에서 경고를 냅니다.

  • Scala 2 스타일의 암시적 변환 정의를 컴파일할 때
  • scala.Conversion의 given 인스턴스가 변환으로 삽입되는 호출 지점(call site)에서

경고를 끄려면 다음 중 하나를 하면 돼요.

  • scala.language.implicitConversions를 다음 스코프 안으로 임포트한다.
    • Scala 2 스타일의 암시적 변환 정의
    • scala.Conversion의 given 인스턴스가 변환으로 삽입되는 호출 지점
  • 컴파일러를 -language:implicitConversions 옵션으로 호출한다

변환이 컴파일러에 의해 적용될 때는 경고가 발생하지 않아요.

더 알아보기