람다식이란?

람다식은 간단하게 말해서 함수를 메서드의 인자로 전달하는 것을 가능하게 한다.

하나의 메서드만 포함하는 함수형 인터페이스와 같이 구현이 매우 간단한 경우 클래스를 정의하여 사용할 경우 코드가 복잡해질 수 있다. 이러한 경우 람다식을 사용하면 단순하게 표현할 수 있다.

() -> {} 와 같은 모습으로 사용되며 ()에는 함수에 사용될 인자가 들어가고, {}에는 실행할 함수의 코드가 들어간다.

람다식의 장점

- 코드가 간결해진다.

- 표현식에 개발자의 의도가 들어나 가독성이 좋다.

- 함수의 이름을 정할 필요가 없다.

- 병렬 처리가 가능하다

람다식의 단점

- 무작정 사용할 경우 코드가 난잡해질 수 있다.

- 람다로 작성한 무명함수는 재사용할 수 없다.

- 재귀로 사용하기에 부적합하다.

람다식의 문법

람다식은 다음으로 구성된다.

  • 매개변수는 괄호로 묶인 부분에 쉼표로 구분되어 포함되며 매개변수의 특징은 다음과 같다.
    1. 람다식에서 매개변수의 데이터 타입을 생략할 수 있다.
    2. 매개변수가 하나만 있는 경우 괄호를 생략할 수 있다. 아래와 같이 표현할 수 있다.
p -> p.getGender() == Person.Sex.MALE 
    && p.getAge() >= 19 
    && p.getAge() <= 35
  • 화살표 토큰 ->
  • 단일 표현식 또는 명령문 블록으로 구성된 본문. 

단일 표현식을 사용하면 자바 런타임이 식을 평가하여 값을 리턴하거나 리턴문을 사용할 수도 있다.

p -> {
    return p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25;
}

다음은 람다식을 사용한 예이다.

public class Calculator {
  
    interface IntegerMath {
        int operation(int a, int b);   
    }
  
    public int operateBinary(int a, int b, IntegerMath op) {
        return op.operation(a, b);
    }
 
    public static void main(String... args) {
    
        Calculator myApp = new Calculator();
        IntegerMath addition = (a, b) -> a + b;
        IntegerMath subtraction = (a, b) -> a - b;
        System.out.println("40 + 2 = " +
            myApp.operateBinary(40, 2, addition));
        System.out.println("20 - 10 = " +
            myApp.operateBinary(20, 10, subtraction));    
    }
}

operateBinary 메서드는 두 정수형 파라미터에 수학적 계산을 수행한다. 계산은 IntegerMath의 인스턴스에 의해 지정된다. 람다식을 통해 addtion 과 subtraction 계산이 정의되어 있다. 출력 결과는 다음과 같다.

40 + 2 = 42
20 - 10 = 10

람다 표현식의 사용 사례

이 사례에서는 로컬 및 익명 클래스를 사용한 방식을 람다식을 사용하여 효율적이고 간결하게 표현하는 것을 목표로 한다.

소셜 네트워킹 애플리케이션을 만든다고 해보자. 관리자가 특정 구성원에게 메시지 보내기는 기능을 만들려고 한다. 다음 표에서는 이 사용 사례를 자세히 설명한다.

필드 설명
이름 선택한 구성원에 대한 작업 수행
주체 관리자
전제조건 관리자는 시스템에 로그인 되어있다.
사후조건 지정된 기준에 부합하는 멤버에 대해서만 작업이 수행된다.
주요 성공 시나리오 1. 관리자는 특정 작업을 수행할 멤버의 기준을 정한다.
2. 관리자는 선택한 구성원에 대해 수행할 작업을 지정한다.
3. 관리자가 제출 버튼을 선택한다.
4. 시스템은 지정된 기준과 일치하는 모든 구성원을 찾는다.
5. 시스템은 일치하는 모든 구성원에 대해 지정된 작업을 수행한다.

소셜 네트워킹 애플리케이션의 구성원이 다음 Person 클래스로 표시된다고 가정한다.

public class Person {

    public enum Sex {
        MALE, FEMALE
    }

    String name;
    LocalDate birthday;
    Sex gender;
    String emailAddress;

    public int getAge() {
        // ...
    }

    public void printPerson() {
        // ...
    }
}

애플리케이션의 구성원은 List<Person>에 저장된다고 가정하자.

 

접근 1 : 하나의 특징이 일치하는 멤버를 찾는 메서드를 생성한다.

쉬운 방법은 각자 메서드를 생성하는 것 이다. 각 메서드는 성별이나 나이와 같은 하나의 특징이 일치하는 멤버를 찾는다. 다음 메서드는 기준 나이보다 나이가 많은 구성원을 출력한다.

public static void printPersonsOlderThan(List<Person> roster, int age) {
    for (Person p : roster) {
        if (p.getAge() >= age) {
            p.printPerson();
        }
    }
}

 

접근2: 보다 일반화된 검색 메서드를 생성한다.

다음의 메서드는 접근1의 printPersonsOlderThan보다 일반적이다. 이 메서드는 지정된 연령 범위 내의 구성원을 출력한다.

public static void printPersonsWithinAgeRange(
    List<Person> roster, int low, int high) {
    for (Person p : roster) {
        if (low <= p.getAge() && p.getAge() < high) {
            p.printPerson();
        }
    }
}

 

접근 3 : 로컬 클래스에서 검색 기준을 지정한다.

다음 메서드는 지정한 기준에 맞는 구성원을 출력한다.

public static void printPersons(
    List<Person> roster, CheckPerson tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }

이 메서드는 파라미터 CheckPerson tester 에서 tester.test()를 호출함으로서 List<Person> roster의 Person인스턴스들이 tester에 지정된 검색 기준을 만족하는지 검사한다.

 

검색 기준을 지정하기 위해 CheckPerson 인터페이스를 구현한다.

interface CheckPerson {
    boolean test(Person p);
}

 

다음 클래스는 test메서드를 구현함으로서 CheckPerson 인터페이스를 구현합니다. 이 메서드는 군대에 갈 자격이 있는 사람을 필터링한다.

class CheckPersonEligibleForArmy implements CheckPerson {
    public boolean test(Person p) {
        return p.gender == Person.Sex.MALE &&
            p.getAge() >= 19 &&
            p.getAge() <= 35;
    }
}

 

printPersons 메서드를 호출할 때 CheckPersonEligibleForArmy 인스턴스를 생성하여 인자로 전달하여 사용한다.

printPersons(
    roster, new CheckPersonEligibleForArmy());

 

접근 4 : 익명 클래스를 사용하여 검색 기준 지정

printPersons의 매개변수로 익명 클래스를 사용할 수 있다.

printPersons(
    roster,
    new CheckPerson() {
        public boolean test(Person p) {
            return p.getGender() == Person.Sex.MALE
                && p.getAge() >= 19
                && p.getAge() <= 35;
        }
    }
);

이러한 방식은 각 검색 기준마다 새로운 클래스를 만들 필요를 없애 코드의 길이를 줄여준다.하지만 익명 클래스는 CheckPerson 인터페이스가 하나의 메서드만 포함한 경우에만 사용 가능하다. 이 경우에 익명 클래스 대신에 람다식을 사용할 수 있다.

 

접근 5 : 람다식을 사용하여 검색 기준 지정

CheckPerson 인터페이스는 함수형 인터페이스이다. 함수형 인터페이스는 하나의 추상 메서드만 포함하는 인터페이스를 말한다. 추상 메서드가 하나만 존재하기 때문에 구현을 할 때 해당 메서드의 이름을 생략할 수 있다. 

printPersons(
    roster,
    (Person p) -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 19
        && p.getAge() <= 35
);

 

접근 6 : 람다식과 표준 함수형 인터페이스 사용

CheckPerson 인터페이스를 다시 살펴보자.

interface CheckPerson {
    boolean test(Person p);
}

이는 하나의 추상 메서드를 포함하는 함수형 인터페이스다. 이 메서드는 boolean 타입을 반환하고 하나의 매개변수를 전달받는다.  이 메서드는 너무 간결해서 정의할 가치가 없을지도 모른다. 따라서 JDK는 java.util.function에 몇몇 표준 함수형 인터페이스를 제공하며 이를 사용하여 대부분의 코드를 구현할 수 있다. 

 

예를들어, CheckPerson 대신에 Predicate<T> 인터페이스를 사용할 수 있다.이 인터페이스는 boolean test(T t)를 포함한다.

interface Predicate<T> {
    boolean test(T t);
}

Predicate<T> 인터페이스는 제네릭 인터페이스의 한 예이다. 제네릭 타입은 <>안에 하나 이상의 타입 매개변수를 지정한다. 이 인터페이스는 하나의 타입 매개변수 T만 포함한다. 실제 타입 인자로 제네릭 타입을 선언하거나 인스턴스화하면 그것이 파라미터화된 타입이다. 예를들어 다음은 매개변수화된 타입 Predicate<Person> 이다.

interface Predicate<Person> {
    boolean test(Person t);
}

이 매개변수화된 타입은 CheckPerson의 test와 같은 리턴타입과 파라미터를 리턴하는 메서드를 포함한다. 결과적으로 다음과 같이 CheckPerson 대신에 Predicate<T>를 사용할 수 있다.

public static void printPersonsWithPredicate(
    List<Person> roster, Predicate<Person> tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }
}

결과적으로 람다식으로 표현한 아래의 메서드 호출은 접근3에서 호출한 printPersons와 같다.

printPersonsWithPredicate(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 19
        && p.getAge() <= 35
);

이 메서드에서 이 방식만이 람다식을 사용하는 유일한 방법은 아니다. 다음 접근은 새로운 람다식 사용법을 제안한다.

 

접근7 : 애플리케이션 전체에서 람다식 사용

다른 람다식을 사용할 수 있는 곳을 찾기 위해 printPersonsWithPredicate 를 다시 보자

public static void printPersonsWithPredicate(
    List<Person> roster, Predicate<Person> tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }
}

printPerson 메서드를 호출하는 대신에, tester의 기준을 만족하는 Person 인스턴스에 다른 작업을 지정할 수 있다.

이 작업은 람다식으로 구현할 수 있다. 하나의 매개변수(Person 타입의 객체)를 받고 void를 리턴하는 printPerson과 비슷한  람다식을 원한다고 가정하자. 기억하라, 람다식을 사용하기 위해서 함수형 인터페이스를 구현해야한다. 이 경우에 Person 타입의 매개변수를 받고 void를 리턴하는 함수형 인터페이스가 필요하다. Consumer<T> 인터페이스는 그러한 특징을 가지는 메서드 void accept(T t)를 포함한다. 다음 메서드는 Consumer의 인스턴스 accept를 호출함으로서 p.printPerson()의 호출을 대신한다.

public static void processPersons(
    List<Person> roster,
    Predicate<Person> tester,
    Consumer<Person> block) {
        for (Person p : roster) {
            if (tester.test(p)) {
                block.accept(p);
            }
        }
}

결과적으로 아래의 메서드 호출은 접근3에서 printPersons를 호출한 것과 같다.

processPersons(
     roster,
     p -> p.getGender() == Person.Sex.MALE
         && p.getAge() >= 19
         && p.getAge() <= 35,
     p -> p.printPerson()
);

멤버의 프로필에 대해 출력하는 것 이외에 다른 작업을 더 하고 싶다면 어떻게 할 것인가. 멤버의 프로필을 검증하거나 연락처 정보를 검색한다고 가정하자. 이 경우에, 값을 리턴하는 함수형 인터페이스가 필요하다. Function<T, R> 인터페이스는 메서드 R apply(T t)를 포함한다. 다음 메서드는 매개변수 mapper에 지정된 데이터를 검색하고 매개변수 block에 지정된 작업을 수행한다.

public static void processPersonsWithFunction(
    List<Person> roster,
    Predicate<Person> tester,
    Function<Person, String> mapper,
    Consumer<String> block) {
    for (Person p : roster) {
        if (tester.test(p)) {
            String data = mapper.apply(p);
            block.accept(data);
        }
    }
}

아래의 메서드 호출은 병역에 적합한 roster의 멤버에 포함된 이메일 주소를 검색하고 출력한다.

processPersonsWithFunction(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 19
        && p.getAge() <= 35,
    p -> p.getEmailAddress(),
    email -> System.out.println(email)
);

접근 8: 제네릭을 사용한 확장

processPersonsWithFunction 메서드를 다시 보자. 다음은 모든 데이터 유형의 요소를 포함하는 컬렉션을 매개변수로 받는 일반화된 버전이다.

public static <X, Y> void processElements(
    Iterable<X> source,
    Predicate<X> tester,
    Function <X, Y> mapper,
    Consumer<Y> block) {
    for (X p : source) {
        if (tester.test(p)) {
            Y data = mapper.apply(p);
            block.accept(data);
        }
    }
}

병역에 적합한 구성원의 e-mail 주소를 출력하기 위해 다음과 같이 processElements 메서드를 호출할 수 있다. (접근 7에서 호출한 것과 같음)

processElements(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 19
        && p.getAge() <= 35,
    p -> p.getEmailAddress(),
    email -> System.out.println(email)
);

이 메서드 호출은 다음 작업들을 수행한다.

    1. Iterable<X> source 로 부터 객체의 소스를 얻는다. 이 예제에서는 roster 로 부터 Person 객체의 소스를 얻는다. Iterable 인터페이스는 Collection 의 상위 인터페이스이기 때문에 List 타입인 roster는 Iterable 타입의 객체이다.

    2. Predicate 객체 tester 에 매치하는 객체를 필터링한다. 이 예제에서는 Predicate 객체가 구성원이 병역에 적합한지 검사하는 람다식이다.

    3. Function 객체 mapper 에 의해 지정된 값에 필터링된 객체를 매핑한다. 이 예제에서 Function 객체는 구성원의 e-mail 주소를 리턴하는 람다식이다.

    4. Consumer 객체 block 에 의해 지정된 작업을 각 매핑된 객체에 수행한다. 이 예제에서 Consumer 객체는 Function 객체가 리턴한 e-mail 주소가 저장된 string을 출력하는 람다식이다.

 

접근 9 : 파라미터로 람다식을 허용하는 작업에 Aggregate operations 사용

다음 예제는 roster 내의 구성원 중 병역에 적합한 구성원의 e-mail 주소를 출력하기 위해 aggregate operations 를 사용한다.

roster
    .stream()
    .filter(
        p -> p.getGender() == Person.Sex.MALE
            && p.getAge() >= 18
            && p.getAge() <= 25)
    .map(p -> p.getEmailAddress())
    .forEach(email -> System.out.println(email));

다음 표는 processElements 메서드가 수행하는 작업과 aggregate operation 을 매핑한다.

processElementsAction Aggregate Operation
객체의 소스를 얻는다. Stream<E> stream()
Predicate 객체와 비교하여 객체를 필터링한다. Stream<T> filter(Predicate<? super T> predicate)
Function 객체에 지정된 값과 객체를 매핑한다. <R> Stream<R> map(Function<? super T, ? extends R> mapper
Consumer 객체에 지정된 작업을 수행한다. void forEach(Consumer<? super T> action)

위 예제에서 맨 처음 stream() 을 호출함으로써 컬렉션에서 직접 처리하지 않고, stream 에서 요소를 처리한다. 스트림은 Elements 의 sequence 이다. 컬렉션과는 다르게, elements 를 저장하는 데이터 구조가 아니다. 대신에, 스트림은 파이프라인을 통해 컬렉션과 같은 소스의 값을 값을 전달한다. 파이프라인은 이 예제에서의 filter-map-forEach 와 같이 stream 작업의 sequence 이다. 추가로, aggregate operations 는 파라미터로 람다식을 허용한다.

 

 

※ 이 글은 오라클 자바 docs 의 Lambda Expressions 를 참조하여 작성했습니다.

https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html

+ Recent posts