이 장의 내용
- 람다란 무엇인가?
- 어디에, 어떻게 람다를 사용하는가?
- 실행 어라운드 패턴
이 장에서는 람다 표현식을 어떻게 만드는지, 어떻게 사용하는지, 어떻게 코드를 간결하게 만들 수 있는지 설명한다. 또한 자바 8 API에 추가된 중요한 인터페이스와 형식 추론 등의 기능도 확인한다. 람다 표현식과 함께 위력을 발휘하는 새로운 기능인 메서드 참조를 설명한다.
3.1 람다란 무엇인가?
람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화한 것이라고 할 수 있다. 람다 표현식에는 이름은 없지만, 파라미터 리스트, 바디, 반환 형식, 발생할 수 있는 예외 리스트는 가질 수 있다. 람다의 특징을 하나씩 자세히 살펴보자.
익명
- 보통의 메서드와 달리 이름이 없으므로 익명이라 표현한다. 구현해야 할 코드에 대한 걱정거리가 줄어든다.
함수
- 람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수라고 부른다. 하지만 메서드처럼 파라미터 리스트, 바디, 반환 형식, 가능한 예외 리스트를 포함한다.
전달
- 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.
간결성
- 익명 클래스처럼 많은 자질구레한 코드를 구현할 필요가 없다.
람다라는 용어는 람다 미적분학 학계에서 개발한 시스템에서 유래했다. 앞선 글에서 보았듯이 람다를 이용해서 간결한 방식으로 코드를 전달할 수 있다.
람다는 세 부분으로 이루어진다.

파라미터 리스트
- Comparator의 compare 메서드 파라미터(사과 두 개)
화살표
- 화살표(->)는 람다의 파라미터 리스트와 바디를 구분한다.
람다 바디
- 두 사과의 무게를 비교한다. 람다의 반환값에 해당하는 표현식이다.
다음은 자바 8에서 지원하는 다섯 가지 람다 표현식 예제다.
/*
* String 형식의 파라미터 하나를 가지며 int를 반환한다.
* 람다 표현식에는 return이 함축되어 있으므로 return 문을 사용할 필요가 없다.
*/
(String s) -> s.length();
// Apple 형식의 파라미터 하나를 가지며 boolean을 반환한다.
(
(Apple a) -> a.getWeight() > 150;
/*
* int 형식의 파라미터 두 개를 가지며 리턴값이 없다.
* 예제에서 보았듯이 람다 표현식은 여러 행의 문장을 포함할 수 있다.
*/
(int x, int y) -> {
System.out.println("Result:");
System.out.println(x + y);
};
// 파라미터가 없으며 int 42를 반환한다.
() -> 42;
// Apple 형식의 파라미터 두 개를 가지며 int를 반환한다. (반환 비교 결과)
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
람다에는 다음과 같은 표현식 문법이 있다.
표현식 스타일
(parameters) -> expreesion
블록 스타일
(parameters) -> { expreesion; }
아래 예제를 보면서 문법을 제대로 이해해 보자.
앞에서 설명한 람다 규칙에 맞지 않는 람다 표현식을 고르시오.
1. () -> {}
2. () -> "Raoul"
3. () -> { return "Mario"; }
4. (Integer i) -> return "Alan" + i;
5. (String s) -> { "Iron Man"; }
정답
(4, 5번)
1. 파라미터가 없으며 void를 반환하는 람다 표현식이다. 이는 public void run() {}처럼 바디가 없는 메서드와 같다.
2. 파라미터가 없으며 문자열을 반환하는 표현식이다.
3. 파라미터가 없으며 (명시적으로 return 문을 이용해서) 문자열을 반환하는 표현식이다.
4. return은 흐름 제어문이다. (Integer i) -> { return "Alan" + i; }처럼 되어야 올바른 람다 표현식이다.
5. "Iron Man"은 구문(statement)이 아니라 표현식(expression)이다. (String s) -> "Iron Man"처럼 되어야 올바른 람다 표현식이다. 또는 (String s) -> { return "Iron Man"; }처럼 명시적으로 return 문을 사용해야 한다.
3.2 어디에, 어떻게 람다를 사용할까?
이전 예제에서는 Comparator<Apple> 형식의 변수에 할당했다. 2장에서 구현했던 필터 메서드에도 람다를 활용할 수 있었다.
List<Apple> greenApples = filterApples(inventory, (Apple apple) -> Color.GREEN.equals(apple.getColor()));
이 말고도 함수형 인터페이스라는 문맥에서 람다 표현식을 사용할 수 있다. 위 예제에서는 함수형 인터페이스 Predicate<T>를 기대하는 filter 메서드의 두 번째 인수로 람다 표현식을 전달했다. 일단 함수형 인터페이스가 무엇인지 자세히 살펴보자.
3.2.1 함수형 인터페이스
2장에서 만든 Predicate<T> 인터페이스로 필터 메서드를 파라미터화할 수 있었음을 기억하자! 바로 Predicate<T>가 함수형 인터페이스다. Predicate<T>는 오직 하나의 추상 메서드만 지정하기 때문이다.
public interface Predicate<T> {
boolean test(T t);
}
간단히 말해 함수형 인터페이스는 정확히 하나의 추상 메서드를 지정하는 인터페이스다. 지금까지 살펴본 자바 API의 함수형 인터페이스로 Comparator, Runnable 등이 있다.
public interface Comparator<T> {
int compare(T o1, T o2);
}
public interface Runnable {
void run();
}
public interface ActionListener {
void actionPerformed(ActionEvent e);
}
public interface Callable<V> {
V call() throws Exception;
}
public interface PrivilegedAction<T> {
T run();
}
NOTE_ 인터페이스는 디폴트 메서드(인터페이스의 메서드를 구현하지 않는 클래스를 고려해서 기본 구현을 제공하는 바디를 포함하는 메서드)를 포함할 수 있다. 많은 디폴트 메서드가 있더라도 추상 메서드가 오직 하나면 함수형 인터페이스다.
퀴즈를 풀면서 함수형 인터페이스의 개념을 제대로 이해했는지 확인해 보자.
다음 인터페이스 중 함수형 인터페이스는 어느 것인가?
public interface Adder {
int add(int a, int b);
}
public interface SmartAdder extends Adder {
int add(double a, double b);
}
public interface Nothing {
}
정답
Addr만 함수형 인터페이스다.
SmartAdder는 두 추상 add 메서드(하나는 Adder에서 상속받음)를 포함하므로 함수형 인터페이스가 아니다.
Nothing은 추상 메서드가 없으므로 함수형 인터페이스가 아니다.
함수형 인터페이스로 뭘 할 수 있을까? 람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으므로 전체 표현식을 함수형 인터페이스의 인스턴스로 취급(기술적으로 따지면 함수형 인터페이스를 구현한 클래스의 인스턴스)할 수 있다.
다음 예제는 Runnable이 오직 하나의 추상 메서드 run을 정의하는 함수형 인터페이스이므로 올바른 코드다.
// 실제 Runnable 디폴트 인터페이스
public interface Runnable {
public abstract void run();
}
// 람다 사용
Runnable r1 = () -> System.out.println("Hello World 1");
// 익명 클래스 사용
Runnable r2 = new Runnable() {
@Override
public void run() {
System.out.println("Hello World 2");
}
};
public static void process (Runnable r) {
r.run();
}
process(r1); // "Hello World 1" 출력
process(r2); // "Hello World 2" 출력
process(() -> System.out.println("Hello World 3")); // // "Hello World 3" 출력
람사와 익명 클래스를 통해 Runnable 인터페이스의 run을 구현하여 사용하는 예제였다.
3.2.2 함수 디스크립터
함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그니처를 가리킨다. 람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터라고 부른다. 예를 들어 Runnable 인터페이스의 유일한 추상 메서드 run은 인수와 반환값이 없으므로(void 반환) Runnable 인터페이스는 인수와 반환값이 없는 시그니처로 생각할 수 있다.
() -> void 표기는 파라미터 리스트가 없으며 void를 반환하는 함수를 의미한다. 즉, 앞에서 설명한 Runnable이 이에 해당한다. (Apple, Apple) -> int는 두 개의 Apple을 인수로 받아 int를 반환하는 함수를 가리킨다.
public void process(Runnable r) {
r.run();
}
process(() -> System.out.println("This is awesome!!"));
위 코드를 실행하면 "This is awesome!!"이 출력된다. () -> System.out.println("This is awesome!!"))은 인수가 없으며 void를 반환하는 람다 표현식이다. 이는 Runnable 인터페이스의 run 메서드 시그니처와 같다.
process (() -> { System.out.println("This is awesome!!"); });
위 코드처럼 코드를 중괄호로 감싸면 어떨까? 결론적으로 이미 살펴본 것처럼 중괄호는 필요 없다. 자바 언어 명세에서는 void를 반환하는 메서드 호출과 관련한 특별한 규칙을 정하고 있기 때문이다. 즉 한 개의 void 메서드 호출은 중괄호로 감쌀 필요가 없다.
"왜 함수형 인터페이스를 인수로 받는 메서드에만 람다 표현식을 사용할 수 있을까?"라는 의문이 생길 수 있다. 언어 설계자들은 자바에 함수 형식을 추가하는 방법도 대안으로 고려했다. 하지만 언어 설계자들은 언어를 더 복잡하게 만들지 않는 현재 방법을 선택했다. 퀴즈를 풀면서 어디에 람다를 사용할 수 있는가를 잘 이해했는지 확인해 보자.
다음 중 람다 표현식을 올바로 사용한 코드는?
// 1
execute(() -> {});
public void execute(Runnable r) {
r.run();
}
// 2
public Callable<String> fetch() {
return () -> "Trick example ;-)";
}
// 3
Predicate<Apple> p = (Apple a) -> a.getWeight();
정답
1번과 2번은 유효한 람다 표현식이다.
첫 번째 예제에서 람다 표현식 () -> {}의 시그니처는 () -> void며 Runnable의 추상 메서드 run의 시그니처와 일치하므로 유효한 람다 표현식이다. 다만 람다의 바디가 비어있으므로 이 코드를 실행하면 아무 일도 일어나지 않는다.
두 번째 예제도 유효한 람다 표현식이다. fetch 메서드의 반환 형식은 Callable<String>이다. T를 String으로 대치했을 때 Callable<String> 메서드의 시그니처는 () -> String이 된다. () -> "Tricky example;-)"는 () -> String 시그니처이므로 문맥상 유효한 람다 표현식이다.
세 번째 예제에서 람다 표현식 (Apple a) -> a.getWeight()의 시그니처는 (Apple) -> Integer이므로 Predicate<Apple>: (Apple) -> boolean의 test 메서드의 시그니처와 일치하지 않는다. 따라서 유효한 람다 표현식이 아니다.
@FunctionalInterface는 무엇인가?
새로운 자바 API를 살펴보면 함수형 인터페이스에 @FunctionalInterface 어노테이션이 추가되어 있다. @FunctionalInterface는 함수형 인터페이스임을 가리키는 어노테이션이다. @FunctionalInterface로 인터페이스를 선언했지만 실제로 함수형 인터페이스가 아니면 컴파일러가 에러를 발생시킨다.
3.3 람다 활용 : 실행 어라운드 패턴
람다와 동작 파라미터화로 유연하고 간결한 코드를 구현하는 데 도움을 주는 실용적인 예제를 살펴보자. 자원 처리(예를 들면 데이터베이스의 파일 처리)에 사용하는 순환 패턴은 자원을 열고, 처리한 다음에, 자원을 닫는 순서로 이루어진다. 설정과 정리 과정은 대부분 비슷하다. 즉, 실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태를 갖는다. 아래 그림과 같은 코드를 실행 어라운드 패턴이라고 부른다. 아래 코드는 파일에서 한 행을 읽는 코드다.
public String processFile() throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return br.readLine();
}
}

3.3.1 1단계 : 동작 파라미터화를 기억하라
현재 코드는 파일에서 한 번에 한 줄만 읽을 수있다. 한 번에 두 줄을 읽거나 가장 자주 사용되는 단어를 반환하려면 어떻게 해야 할까? 기존의 설정, 정리 과정은 재사용하고 processFile 메서드만 다른 동작을 수행하도록 명령할 수 있다면 좋을 것이다. processFile의 동작을 파라미터화하는 것이다. processFile 메서드가 BufferedReader를 이용해서 다른 동작을 수행할 수 있도록 processFile 메서드로 동작을 전달해야 한다.
processFile 메서드가 한 번에 두 행을 읽게 하려면 코드를 어떻게 고쳐야 할까? 우선 BuffererdReader를 인수로 받아서 String을 반환하는 람다가 필요하다. 다음은 BufferedReader에서 두 행을 출력하는 코드다.
String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());
3.3.2 2단계 : 함수형 인터페이스를 이용해서 동작 전달
함수형 인터페이스 자리에 람다를 사용할 수 있다. 따라서 BufferedReader -> String과 IOException을 던질 수 있는 시그니처와 일치하는 함수형 인터페이스를 만들어야 한다. 이 인터페이스를 BuffererdReaderProcessor라고 정의하자.
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
정의한 인터페이스를 processFile 메서드의 인수로 전달할 수 있다
public String processFile(BufferedReaderProcessor p) throws IOException {
...
}
3.3.3 3단계 : 동작 실행
이제 BufferedReaderProcessor에 정의된 process 메서드의 시그니처 (BufferedReader -> String)와 일치하는 람다를 전달할 수 있다. 람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으며 전달된 코드는 함수형 인터페이스의 인스턴스로 전달된 코드와 같은 방식으로 처리한다. 따라서 processFile 바디 내에서 BufferedReaderProcessor 객체의 process를 호출할 수 있다.
public String processFile(BufferedReaderProcessor p) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return p.process(br);
}
}
3.3.4 4단계 : 람다 전달
이제 람다를 이용해서 다양한 동작을 processFile 메서드로 전달할 수 있다.
다음은 한 행을 처리하는 코드다.
String oneLine = processFile((BufferedReader br) -> br.readLine());
다음은 두 행을 처리하는 코드다.
String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine());
processFile 메서드를 더 유연하게 만드는 과정을 보여준다.
지금까지 함수형 인터페이스를 이용해서 람다를 전달하는 방법을 확인했다. 이때 인터페이스도 정의했다. 다음 절에서는 다양한 람다를 전달하는 데 재활용할 수 있도록 자바 8에 추가된 새로운 인터페이스를 살펴본다.
고생했습니다~
끗!
( 무지무지 뜻 깊은 날)