자바 8에서 도입된 람다 표현식은 등장한 지 꽤 오랜 시간이 지났음에도 여전히 낯설고, 많은 개발자들이 왜 사용해야 하는지 제대로 알지 못하는 기능입니다. 이 글에서는 람다의 장점이 무엇인지 알아보며, 왜 람다를 사용해야 하는지 살펴보겠습니다.
(1) 더 쉽고 간략한 iteration
일반적으로 컬렉션을 조회하는 다음과 같은 코드가 있다고 가정해 보겠습니다.
List<Integer> numList = Arrays.asList(1,2,3,4,5,6,7,8,9);
for(int n : numList){
System.out.println(n);
}
컬렉션을 사용할 때 람다를 사용한다면 아래와 같이 아주 간단하게 표현이 가능합니다.
numList.forEach(System.out::println);
간단하고 코드가 훨씬 짧아지지만, 두 코드는 동일한 작업을 수행합니다. 위 코드는 메서드 레퍼런스를 사용한 것으로, 이를 사용하지 않고 아래와 같이 표현해도 동일한 결과를 얻을 수 있습니다.
numList.forEach(n->System.out.println(n));
// or
numList.forEach((Integer n)->System.out.println(n));
대부분의 경우 람다식의 인수 타입을 컴파일러가 추론할 수 있기 때문에 Integer는 생략 가능합니다.
(2) 작동 방식 전달 가능
자바는 일급 객체가 아니기 때문에 메서드에 또 다른 메서드를 전달하는 것이 불가능합니다. 그러나 람다에서는 이것이 가능합니다.
먼저, 리스트 내 모든 값의 합을 구하는 메서드는 아래와 같습니다.
public int sumOfList(List<Integer> numList){
int total = 0;
for(int n : numList) total += n;
return total;
}
또한 이와 별개로 리스트 내 모든 짝수의 합만 구하는 메서드는 아래와 같습니다.
public int sumEvenOfList(List<Integer> numList){
int total = 0;
for(int n : numList) {
if(n % 2 == 0) total += n;
}
return total;
}
또 이번에는 리스트 내에 3보다 큰 수의 합만 구하는 메서드를 구현해야 한다고 가정해본다면, 이제 리팩토링이 필요해진다는 것을 알 수 있습니다. 코드는 점점 지저분해지고 있는 중이며, 중복된 코드가 늘어나고 있기 때문입니다.
이를 람다를 사용한다면 아래와 같이 표현할 수 있습니다.
public int sumOfList(List<Integer> numList, Predicate<Integer> p) {
int total = 0;
for(int n : numList){
if(p.test(n)) {
total += n;
}
}
return total;
}
sumOfList(numList, n->true);
sumOfList(numList, n->n%2 == 0);
sumOfList(numList, n->n>3);
조건을 판단하기 위한 Predicate 객체를 메서드에 전달함으로써, 훨씬 심플한 코드를 작성할 수 있습니다.
(3) 지연 연산
이는 마찬가지로 람다의 큰 이점 중 하나입니다. 컬렉션에는 stream()이라는 메서드가 존재하는데, 이 메서드를 호출하게 되면 컬렉션을 스트림으로 만들 수 있습니다.
스트림은 컬렉션과 다른 점이 몇 가지 존재하는데 아래와 같습니다.
- 값을 저장하지 않는다. 연산 파이프라인을 거치는 데이터 구조를 통해 값을 나르기만 할 뿐이다.
- 원본을 수정하지 않는다. 스트림 연산 결과는 산출되지만, 입력 데이터 소스는 수정하지 않는다.
- 지연 연산 추구. 필요한 만큼 스트림에서 요소를 검사한다.
- 클라이언트 쪽에서 필요한 만큼만 값을 가져갈 수 있다.
리스트에 있는 짝수 원소 중에서 두 배 했을 때 5보다 커지는 첫 번째 수를 찾고 싶다고 가정하겠습니다.
for(int n : numList) {
if(n % 2 == 0){
int doubleN = n*2;
if(doubleN > 5) {
System.out.println(n);
break;
}
}
}
for와 if가 3 중첩되어 너무 많은 작업이 이루어지고 있습니다. 이 코드는 각자 역할에 따라 메서드로 리팩토링하고, 각각 for 루프 돌리도록 아래와 같이 변경할 수도 있습니다.
public boolean isEven(int n) {
System.out.println("isEven : "+n);
return n % 2 == 0;
}
public int doubleIt(int n) {
System.out.println("doubleIt : "+n);
return n* 2;
}
public boolean isGreaterThan5(int n) {
System.out.println("isGreaterThan5 : "+n);
return n> 5;
}
//호출부
List<Integer> l1 = new ArrayList<Integer>();
for (int n : numList) {
if (isEven(n)) l1.add(n);
}
List<Integer> l2 = new ArrayList<Integer>();
for (int n : l1) {
l2.add(doubleIt(n));
}
List<Integer> l3 = new ArrayList<Integer>();
for (int n : l2) {
if (isGreaterThan5(n)) l3.add(n);
}
System.out.println(l3.get(0)/2);
그러나 코드가 장황하고 불필요한 연산이 수행됩니다.
isEven : 3
isEven : 4
isEven : 5
isEven : 6
isEven : 7
isEven : 8
isEven : 9
doubleIt : 2
doubleIt : 4
doubleIt : 6
doubleIt : 8
isGreaterThan5 : 4
isGreaterThan5 : 8
isGreaterThan5 : 12
isGreaterThan5 : 16
4
이를 스트림을 이용하게 되면 아래와 같이 표현할 수 있습니다.
System.out.println(numList.stream()
.filter(Test::isEven)
.map(Test::doubleIt)
.filter(Test::isGreaterThan5)
.findFirst().get()/2);
지연 연산을 사용하기 때문에 CPU의 낭비가 없고, findFirst가 수행되기 전까지는 앞의 어떠한 연산도 수행하지 않습니다.
isEven : 1
isEven : 2
doubleIt : 2
isGreaterThan5 : 4
isEven : 3
isEven : 4
doubleIt : 4
isGreaterThan5 : 8
4
출력된 결과를 확인해 보면, 람다를 사용한 경우 전체 loop를 다 도는 것이 아니라, 우리가 원하는 해답을 찾은 경우 스트림이 종료되는 것을 볼 수 있습니다.
(findFirst는 결과가 존재하지 않을 수 있는 가능성 때문에 Optional 객체에 값을 담아 반환합니다. 따라서 get 메서드를 통해 값을 꺼내어 출력하였습니다.)
자바 람다에 대한 더욱 자세한 내용은 Java Tutorials - Lambda Expressions에서 확인하실 수 있습니다.
[references]
Why We Need Lambda Expressions in Java - Part 2
Why We Need Lambda Expressions in Java - Part 1
Java Tutorials - Lambda Expressions
Lambda Expressions and Functional Interfaces: Tips and Best Practices
'Java' 카테고리의 다른 글
Static 사용을 피해야 하는 이유 (1) | 2020.10.20 |
---|---|
Lambda, 무엇이 단점일까? (0) | 2020.10.20 |
자바7 업데이트 - 숫자 리터럴 구분자 (0) | 2020.10.20 |
[Refactoring] if문 (0) | 2020.10.20 |
Map의 Value 얻기 - KeySet => EntrySet (0) | 2020.10.12 |
댓글