본인은 주로 Java 개발을 주업무로 하고 있다. Java 개발자로서 Scala 학습하면서 몇 가지 마법 같은(?) 기능을 접할 때마다 놀라움과 당혹감을 감출 수가 없다. 옮고 그름을 넘어 신비로울 정도이다.
본 글에서는 본인이 이해한 수준의 Implicit에 대해서 정리해보고자 한다.

암시적 : implicit

암시적(implicit)이란 무엇일까? 장난 같지만 “명시적(Explicit)”의 반대말이다. 프로그램 세계에서는 정의나 행위를 직접 언급 또는 기술하는 것이 아니라 당연시되어 내용을 생략하거나 합의하에 생략한다는 느낌정도의 표현이다.
Java의 묵시적 타입 변환이나, 생성자가 하나도 없을 때 생성되는 디폴트 생성자가 그 예이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
jshell> int xInt = 4
xInt ==> 4

jshell> float xFloat = 4.4F
xFloat ==> 4.4

jshell> xFloat = xInt
xFloat ==> 4.0

jshell> xInt = xFloat
| Error:
| incompatible types: possible lossy conversion from float to int
| xInt = xFloat
| ^----^

jshell> xInt = (int)xFloat
xInt ==> 4

위 예제 코드는 Java에서 타입 변환을 보여주는 코드이다. float xFloat = xInt에서 처럼 타입이 다른 변수의 값이 할당되거나 연산이 발생할때 정보의 손실이 발생하지 않을 경우 자동으로 타입 변환이 이루어진다. 하지만 zInt = xFloat에서는 소수점 이하의 값을 처리할 수 없으므로 정보 손실이 발생하고 오류가 발생한다. 정보 손실이 발생하는 경우에도 명시적으로 타입 변환을 할 경우 정보 손실을 무시하고 진행할 수도 있다.

Scala에서는 이런 암시적 자동화 기능을 많이 제공한다. 언어 차원에서 예측할 수 있는 부분은 알아서 처리해준다. 나아가 implicit 키워드를 사용해서 개발자가 암시적으로 처리해 달라고 언어차원으로 요청할 수도 있다. 그 예가 아래 세가지이다.

암시적 파라미터 : Implicit parameter

Scala tutorials : 암시적 파라미터.

우선 파라미터(parameter) 의미를 명확하게 정의하고 넘어가자. 파라미터는 함수에 정의 시 나열되는 변수를 말한다. 우리말로 매개변수라고 번역을 한다. 비슷하게 쓰이는 단어가 인자(argument)인데 함수 호출 시 전달되는 값들을 말한다. 동일한 표현같지만 전혀 다른 의미이다.
Scala는 메쏘드나 함수의 파라미터 변수에 implicit 키워드를 붙이면 호출 시 생략 가능하다. 메쏘드 혹은 함수 호출 시 파라미터가 생략될 경우 자동으로 해당 파타미터의 값을 컴파일러가 제공한다.
이런 제공되는 인자 값은 어디서 가져올까? 컴파일러는 아래 두 조건에 만족하는 값에서 가져와서 인자 값을 채우게된다.

implicit 변수

메쏘드가 호출되는 시점의 Scope에서 Profix가 없이 즉 변수명으로만 접근할 수 있는 implicit parameter 혹은 implicit 키워드로 정의된 동일 타입의 변수. 변수명으로만 접근 할 수 있는 동일타입 변수여야 컴파일러가 자동으로 넣어준다.

  • 단순 implicit 변수
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@  implicit val a:Int = 10
a: Int = 10

@ def plus (b:Int)(implicit d:Int) = b + d
defined function plus

@ plus(1)
res5: Int = 11

@ implicit val c:Int = 10
c: Int = 10

@ plus(1)
cmd7.sc:1: ambiguous implicit values:
both value a in object cmd3 of type => Int
and value c in object cmd6 of type => Int
match expected type Int
val res7 = plus(1)
^
Compilation Failed
  • 호출 시점의 implicit 파라미터
1
2
3
4
5
6
7
8
9
10
11
@ class PlusTest {
def plus (m:Int)(implicit n:Int) = m + n
def callPlus(m:Int)(implicit o:Int) = plus(m)
}
defined class PlusTest

@ val test = new PlusTest
test: PlusTest = ammonite.$sess.cmd3$PlusTest@1bf0f6f6

@ test.callPlus(1)(10)
res5: Int = 11

여기서 아래 두가지 유의점을 조심해야 한다.

  • 호출되는 시점에 implicit로 선언된 동일 변수가 하나여야만 한다. 중복 시 오류가 발생한다.
  • 정의된 영역이 아닌 호출되는 영역에 implicit 변수가 정의되어 있어야 한다. 클래스 멤버가 implicit이고 클래스 멤버 메쏘드에서 해당 implicit 멤버를 암묵적 파라미터 인자로 넣을 수 없다. implicit 멤버는 클래스 인스턴드명의 Prefix가 필요하기 때문에 적용될 수 없다.

implicit Companion Object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@ trait Add[T] { def apply(m:T, n:T):T }
defined trait Add

@ implicit object intAdd extends Add[Int] {
def apply(m:Int, n:Int) = m + n
}
defined object intAdd

@ implicit object doubleAdd extends Add[Double] {
def apply(m:Double, n:Double) = m + n
}
defined object doubleAdd

@ def plus[T](m:T, n:T)(implicit f:Add[T]) = f(m, n)
defined function plus

@ plus (1.0,2.2)
res31: Double = 3.2

@ plus (1,2)
res32: Int = 3

예제 코드와 같이 object 키워드로 생성된 Companion Object 객체의 타입이 암시적 파라미터의 타입과 동일할 때 자동으로 인자 값으로 넣어준다.

파라미터 인자값을 자동으로 채워 준다는 의미에서 implicit parameter는 어떤면에서 디폴트 파라미터나 DI(Dependency Injection, 의존성 주입) 냄새가 난다. 하지만 더 부러럽고 유연성있고 멋지게 동작한다는 느낌이다.

암시적 변환 : Implicit Conversions

Scala tutorials : 암시적변환

Scala는 strong typed language이고, 일반 타입에 대한 암묵적 타입을 기본적으로 제공하지 않는다. 그럼 아래 코드는 무엇이란 말인가?

1
2
3
4
5
6
7
8
@ val intValue:Int = 10
intValue: Int = 10

@ val doubleValue:Double = intValue
doubleValue: Double = 10.0

@ val doubleValue:Double = intValue + 10.0
doubleValue: Double = 20.0

코드에서는 타입 변환이 암묵적으로 이루어지고 있다. 그럼 암묵전인 변환이 기본 제공하지 않는다는 것은 틀린말인가?
결론은 맞으면서도 틀리다.
Scala는 암시적 변환이라는 기능을 제공하고 이것을 사용하여 기본 API에서 여러 개의 타입에 대해서 변환 로직을 모두 구현해 놓았다. 언어 차원에서 지원하지 않지만 SDK 차원에서 구현이 되어 있는 것이다.

그럼 어떤 메커리즘으로 동작하는지 보자.

  1. A=>B 타입 변환이 필요하다.
  2. implicit 키워드로 정의된 A=>B 타입을 바꾸는 단항 함수가 존재하는지 검색한다.
  3. 존재하면 해당 함수를 사용해서 타입을 변환한다.

구현 예를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
@ class A { val a:String = "A" }
defined class A

@ class B { val b:String = "B" }
defined class B

@ implicit def convertAtoB(aIns:A):B = { println("call convert func"); new B}
defined function convertAtoB

@ val b:B = new A
call convert func
b: B = ammonite.$sess.cmd17$B@5d68be4f

예제 코드를 보면 전혀 다른 클래스의 인스턴트가 변환되어 할당되는 것을 볼 수가 있다. 그리고 그 과정에서 implicit converter 함수인 convertAtoB가 호출되는 것을 알 수 있다.
Scala는 이런 암시적 변환을 위해서 암시적 변환 함수를 미리 제공해 놓았다고 앞에서 말했다. 암시적으로 임포트(자동으로 패키지가 로드됨) 되는 오브젝트 scala.Predef에 그런 암시적 변환 함수가 미리 선언되어 있다.

암시적 변환은 불필요한 코드를 줄이는 효과를 가져오지만 잠재적 위험 요소도 가지고 있다. 개발자가 의도하지 않은 상황에서 타입 변환이 일어날 경우가 그것이다. 그래서 컴파일 시 암시적 타입 변환이 일어날 경우 경고가 발생한다.

암시적 클래스 : implicit class

implicit 키워드를 클래스 앞에 붙이면 해당 클래스의 디폴트 생성자를 implicit converter 함수처럼 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@ object Helpers {
implicit class IntWithTimes(x: Int) {
def times[A](f: => A): Unit = {
def loop(current: Int): Unit =
if(current > 0) {
f
loop(current - 1)
}
loop(x)
}
}
}
defined object Helpers

@ 5 times println("HI")
cmd7.sc:1: value times is not a member of Int
val res7 = 5 times println("HI")
^
Compilation Failed

@ import Helpers._
import Helpers._

@ 5 times println("HI")
HI
HI
HI
HI
HI

예제 코드 참조 페이지

사용 방법은 위 코드와 같이 클래스 선언 시 implicit 키워드를 붙이면 선언이 완료된다.
그리고 import Helpers._로 호출 시점의 같은 scope에 암시적 클래스가 존재하게 만들어주면 된다. 코드 중간에 import 없이 호출하면 오류가 발생한다.
추가적으로 아래 룰을 지켜야 한다.

  • trait/class/object 내부에 선언되어야 한다.
  • 하나의 implicit 인자 값을 가질 수 있다.
  • 같은 scope 내에서 같은 이름의 변수/클래스/object 가 존재해서는 안된다.

그럼 implicit class는 왜 사용하는 것일까? 코드 예제를 보면 5Int 클래스의 객체인데 IntWithTimes의 멤버를 호출하고 있다. 클래스가 확장된 것처럼 보인다. 그렇다. implicit class는 기존 클래스를 수정 없이 확장하는 것(?) 같은 효과를 내기 위해서 사용한다.
실재로는 원본 타입을 위에서 말했듯이 변환한다. 변환 수 내부 멤버 메쏘드를 호출할 수 있게 만들어 준다. 실제 코드 역시 내부적으로 클래스명과 동일 이름의 implicit converter 함수가 추가된다고 한다. 동일 이름의 implicit converter 추가되기 때문에 위의 제약 조건이 발생한다고 한다. 결과적으로는 확장한 것처럼 보인다.

마무리

코딩에서 자동이란 단어는 두가지 면을 가지고 있다고 본다. 알아서 해주니 신경 쓰지 않아도 된다는 면이 있지만 반대로 내용을 모르면 자동으로 해주는 것 자체를 이해를 할 수 없다. 코드에서 생략된 부분이 먼지 모르고 해당 코드를 이해할 수도 없고 사용할 수도 없다. 코드는 잘 작동은 하지만 이해를 할 수 없으니 응용을 할 수 없다.
초보자에게 이런 부분은 높은 진입 장벽이다. 하지만 그 벽을 넘으면 낙원(?)이 기다리고 있을 것이다.