Java 8의 람다 함수 살펴보기
java.util.Map의 method API를 살펴보면 map에서 하나의 항목을 가져오기 위한 방법이 여러가지가 있음을 알 수 있다. 대표적으로 get
, getOrDefault
, compute(computeIfAbsent
, computeIfPresent
)가 있는데 이 문서에서는 어느 것을 사용하는 것이 가장 좋은가를 살펴보고자 한다.
전화번호부 프로그램 리팩토링
csv 파일에서 전화번호 목록을 가져와 전화번호부를 만드는 자바 코드를 생각해보자.
// phonenumbers.csv
홍길동,010-1234-5678
임꺽정,010-1111-2222
홍길동,010-5656-2323
...
위와 같이 특정 파일에서 전화번호를 불러와 list에 삽입하고, 그 list를 map에 넣는 방식으로 작성할 수 있다.
위의 코드 중
이 부분은 Java SE 8에 추가된 메소드 getOrDefault
를 이용하여 다음과 같이 리팩토링 될 수 있다.
기존에 5줄로 작성하던 코드를 3줄로 줄이는 효과를 얻을 수 있다. 그러나 이 코드는 필요하지 않은 경우에도 항상 “new ArrayList()
”를 실행한다는 문제점이 존재한다.
이 문제를 해결하기 위해 computeIfAbsent
메소드를 이용할 수 있다.
map.computeIfAbsent(values[0], k -> new ArrayList()).add(values[1]);
이렇게 하면 위에서 언급한 문제를 해결했을 뿐만 아니라 3줄의 코드를 단 1줄에 표현할 수 있게 되었다.
이로써 전화번호부 프로그램은 아무런 문제가 없는 아름다운 코드가 된 것일까?
최종 PhoneBook은 하나의 인터페이스를 작성하여 해당 구현만 변경하면 되도록 작성하였다.
완성된 PhoneBook 코드 보기
성능 비교
각 방식에 따른 성능 차이를 비교해보기 위해 다음과 같이 테스트 코드를 작성해보았다.
각 방식에 따른 성능을 측정하기 위해 테스트 반복 횟수를 100번, 1000번, 10000번 수행하면서 YourKit Java Profiler로 각 메소드의 실행 시간을 측정하였다. (추가: 테스트를 시행할 때는 테스트 하는 부분이 아닌 코드는 주석으로 처리했다.)
위의 결과에 미루어 보면 반복 횟수가 적을 때는 성능의 차이가 거의 없거나 GetOrDefault
의 성능이 좋다.
그러나 반복 횟수가 커지면 커질 수록 ComputeIfAbsent
의 성능이 가장 좋고 If
, GetOrDefault
는 비슷한 성능을 보인다는 것을 알 수 있다.
(추가: 테스트 데이터는 키의 중복이 높은 데이터셋으로 준비하였다. 따라서 GetOrDefault
의 성능이 비교적 좋지 않다.)
왜 이와 같은 결과가 나타난 것일까? 그 이유를 찾기위해 각 방식 별로 바이트 코드를 비교하여 분석해보자.
바이트 코드를 통한 비교 분석
바이트 코드 레벨에서 살펴보면 처음 두 개의 get
, getOrDefault
는 labmda 함수를 사용한 computeIfAbsent
보다 bytecode instruction 길이가 길다. 그러나 이 때문에 lambda 함수가 다른 빠르다고 할 수는 없다. 이를 조금 더 정확히 파악하기 위해서는 lambda 함수에 대해 살펴볼 필요가 있다.
lambda 함수에는 숨은 instruction이 있다. lambda 함수를 실행하기 위해서는 일련의 준비 작업이 필요하다. 다음 그림을 보면서 그 작업 순서를 살펴보자.
위의 bytecode와 그림에서 보이는 invokedynamic
은 동적 언어를 지원하기 위해 Java SE 1.7에서부터 지원하는 (special) instruction이다.
동적으로 작성된(람다) 함수는 invokedynamic
-> bootstrap method
-> method handles
-> target method
의 순서에 따라 static 함수로 정의된다.
이후에는 bootstrap method
-> method handles
작업은 필요없이 invokedynamic
-> target method
로 바로 호출이 가능하다.
때문에 실행 횟수가 적을 때는 lambda 함수가 다른 방법(get
, getOrDefault
)과 비슷하거나 더 좋지 않은 성능을 보였던 반면 실행 횟수가 많아질 수록 더 좋은 성능을 보여주는 것을 확인할 수 있다. 즉, lambda는 초기 비용이 있지만 반복하여 사용하면 일정한 성능을 보인다.
따라서 “lambda 함수를 남용하는 것은 성능상에 문제를 가져올 수 있지만, 적당한 시기에 잘 사용하면 성능상에 득을 얻을 수 있다”는 결론을 얻을 수 있었다.
여기까지 get
, getOrDefault
, computeIfAbsent
를 서로 비교해보면서 때와 상황에 따라 적절한 기능을 사용하는 것이 성능에 득이 됨을 확인해보았다.
마지막으로 다양한 invoke instruction의 종류, 역할과 순서를 비교한 그림과 함께 이 글을 마친다.