본문으로 바로가기

람다의 내부동작 #1

category 개발이야기/Java 2017. 9. 24. 15:38
반응형

람다의 내부

자바의 lambda는 단순히 익명클래스로 치환되지 않습니다.

bytecode를 확인하면 invokedynamic이란 opcode로 표현됩니다.

이번 포스팅에서는 람다의 내부동작은 어떤지, 왜 익명클래스로 치환되지 않는지를 알아봅니다.


람다의 this

람다의 this와 익명클래스의 this는 어떻게 다를까요?


위 예제에서 같은 동작을 하는 Runnable interface를 하나는 익명클래스, 나머지 하나는 람다로 표현했습니다.

마지막 check()함수는 this로 넘겨바든 객체가 Runnable인지를 확인하여 true/false를 반환합니다.


이 코드를 수행하고 나면 익명클래스는 true 람다는 false를 반환합니다.

즉, 람다는 Runnable을 this로 갖는게 아니라 LambdaThis를 this로 갖습니다.

위 예제를 compile하면 두개의 class가 생성됩니다.


LambdaThis$1은 Runnable의 익명클래스가 하나의 class로 생성되는것을 알수 있습니다.


bytecode상에 빨간 테두리는 익명클래스, 파란 테두리는 lambda를 수행한 부분으로 구분해 놓았습니다.

익명클래스는 invokespecial이란 opcode로 constructor를 호출하고, invokeinterface opcode로 run()을 수행하고 있으나, 람다의 경우 () -> check(this) 이부분이 invokedynamic opcode로 수행된것을 알수 있습니다. 

두개의 명령어에 대한 상세 설명은 다음 포스팅으로 넘깁니다.

여기서는 일단 익명클래스와 람다는 내부적으로 다르게 동작한다라는것만 확인하고 넘어가겠습니다.

Anonymous class & Function type

자바에서는 왜 람다를 내부적으로 익명클래스로 compile하지 않을까요?

Java8 이전 버전에서 람다를 쓰기위한  retrolambda같은 라이브러리나, kotlin같은 언어에서는 compile시점에 람다를 단순히 익명클래스로 치환합니다.

다만, 익명클래스를 사용할 경우 아래와 같은 문제가 발생합니다.

  • 람다식 마다 클래스가 하나씩 생김
  • 항상 새 인스턴스르 할당함.

왜 자바에선 function type을 지원하지 않을까요?

/* Kotlin */

val temp:(Int, Int) -> Int = (a, b) -> a*b


/*Java */

BiFunction<Integer,Integer,Integer> temp = (a, b) -> a*b

코틀린의 경우 function type을 지원하기 때문에 java의 람다보다 더 직관적으로 표현이 가능합니다.

다만 Java architect들이 아래와 같은 고민들을 했다고 합니다.

  • 내부적인 문제들
    • 바이트 수준에서 함수 호출을 어떻게 표현 할 것인가?
    • 함수 타입 변수의 인스턴스는 어떻게 만들것인가?
    • 그외 변성(variance)되는 부분은 어떻게 처리 할 것인가?
  • 대안1: wpsjflrdmfh 표현(List<Strinf> -> int)
    • boxing 문제
    • Language 차원에서의 표현과 VM표현의 갭이 커지는 pain point.
  • 대안2: function type을 만들어 넣는다면?
    • 새로운 함수 시그니처, 새 바이코드 호출, 새 검증규칙..등등 일이 너무 커짐..
    • 복잡도가 너무 크고, corner case가 발생함.
    • 라이브러리가 신형/ 구형으로 분리되고, 두개의 호환문제..

람다의 외부적인 표현 - Functional Interface

람다를 표현할때 interface를 사용하게 된 배경에는 여러 이유가 있습니다.
먼저, 기존부터 추상 메서드가 하나인 interface model을 많이 사용해 왔으므로, 동일한 모델을 사용하여 기존 개발자들의 거부감을 낮출 수 있습니다. 또한 기존에 존재하던 interface들도(Comparator, Runnable, Callable...) Functional interface로 동작하는 이득??이 생겨납니다.

두번째로 타입시스템을 복잡하게 만들지 말고, 람다식을 항ㅅ아 fucntional interface의 instance로 변환한다면 큰 변화없이 java에 람다를 적용할 수 있습니다.

마지막으로 compiler에서 구조적으로 람다식을 functional interface로 인식하고 치환할 수 있습니다.

람다의 내부적인 구현/표현 - MethodHandle

Java7에서 새로운 bytecode tool인 MethodHandle이 추가되었습니다.
MethodHandle은 다음과 같은 특징을 갖습니다.
  • constant pool에 있는 method의 reference를 저장 가능 (LDC로 load 가능)
  • 어떠한 method or field의 handle로 obtain 할수 있다.
  • MH 호출을 통해서 VM이 inline 처리할 수 있다.
  • 강력한 combinator API 제공
    • arguments의 remove, add, reorder
    • arguments 와 return의 boxing, casting
    • 함수의 합성
  • compiler writer에겐 스위스 만능칼!!
Java8 개발 초반에는 Methodhandle을 이용해서 직접 람다를 표현하려고 했으나, MH자체가 VM-model level의 object이며, language level에서 사용시 생략되는 정보가 많다는 문제가 발생합니다.

따라서,먼저 Lambda를 desugaring 해서 method로 표현하고, bytecode signature에서 MethodHandle을 이용해서 lambda를 표현하도록 두개의 표현을 분리합니다.

좀더 이해를 돕기위한 JVM의 opcode들에 대한 설명과 lambda의 동작은 다음번 포스팅에 기재합니다.





반응형

'개발이야기 > Java' 카테고리의 다른 글

Weak reference의 이해  (1) 2017.10.22
람다의 내부동작 #2  (1) 2017.09.26
Java 8 Comparator  (0) 2017.09.14
Java 8 String join  (3) 2017.09.13
Java 8 Lambda Expression - 람다식 #4  (0) 2017.09.12