본문 바로가기
Java

Lambda 무엇이 좋을까?

by kellis 2020. 10. 20.

자바 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 2 - DZone Java

In the first part of this article I started explaining, with hopefully straightforward examples, how the introduction of lambda expressions can make Java a more...

dzone.com

Why We Need Lambda Expressions in Java - Part 1

 

Why We Need Lambda Expressions in Java - Part 1 - DZone Java

Lambda expressions are coming to Java 8 and together with Raoul-Gabriel Urma and Alan Mycroft I started writing a book on this topic.

dzone.com

Java Lambda Expression

Java SE 8: Lambda Quick Start

Java Tutorials - Lambda Expressions

 

Lambda Expressions (The Java™ Tutorials > Learning the Java Language > Classes and Objects)

The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't take advantage of improvements introduced in later releases and might use technology no longer available. See Java Language Changes for a summary of updated

docs.oracle.com

Lambda Expressions and Functional Interfaces: Tips and Best Practices

 

Best Practices using Java 8 Lambdas | Baeldung

Tips and best practices on using Java 8 lambdas and functional interfaces.

www.baeldung.com

 

'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

댓글