Java8 - 람다와 함수형 인터페이스

자바 8에 람다(Lambda)가 도입되면서 자바는 객체지향언어인 동시에 함수형 언어가 될 수 있었다.

람다식을 간단히 표현하면, 메서드를 하나의 식(expression)으로 표현한 익명 함수(Anonymous function) 이다. 여기서 알아두어야 할 점은 람다가 메서드가 아니라 함수라는 점이다.

람다를 알기 전까지는 메서드와 함수를 같은 의미로 해석했지만, 람다를 이해하기 위해서는 이 둘을 분리해서 생각해야 한다. 메서드 역시 함수와 같은 기능을 하지만, 메서드는 클래스에 반드시 속해야 한다는 제약조건이 존재 한다. 그러나 람다에서 함수는 어떤 클래스에도 종속될 필요가 없다. 그냥 사용하면 된다.
Java의 정석 3판 795페이지 참고

자바 8 이전의 자바에서는 메서드를 파라미터로 전달할 수 있는 방식이 없었다. 오직 인스턴스를 생성해서 객체를 통해서만 전달할 수 있었는데, 람다가 도입되면서, 인스턴스 없이도 파라미터안에 메서드를 정의 할 수 있게되었다.

이렇게 함수형 프로그래밍을 하게 될 경우, 객체지향에서처럼 메서드를 객체로 만들어서 파라미터에 넣어 사용하는대신 람다식은 함수 내에서 곧바로 사용할 수 있다는 특징이 있다. 이러한 프로그래밍을 함수형 프로그래밍이라 한다.


함수형 프로그래밍

객체지향 : “Do..”
함수형 : “It is..”

함수형 프로그래밍은 다른 말로 선언형 프로그래밍이라고 한다. 객체지향 프로그래밍이 각각의 객체에게 어떠한 행동을 명령형 프로그래밍이라고 한다면, 함수형 프로그래밍은 하나의 거대한 함수를 디자인하는 기법이다. input에 따라 output이 확정된다는 이야기이다.

객체지향의 경우 각각의 객체에서 개발자가 예측하지 못한 일이 발생한다면, 프로그래머의 기대와 다른 output이 나올수 있지만, 함수형 프로그래밍의 경우엔 함수의 동작에서 변수의 부수적인 값 변경을 원천 배제(=immutable)했기 때문에 input만 동일하다면 언제나 동일한 output을 기대할 수 있다.

즉, 객체 지향이 동작에 집중하는, How에 집중하는 방식의 프로그래밍이라면, 함수형 프로그래밍은 동작을 최소화하고 What 에 집중하는 방식의 프로그래밍이다.


람다식

람다는 코드 한 줄로 작성해서 호출하는 방식의 함수식이다.

1
( parameters ) -> function body

소괄호 안에 파라미터를 정의하고, 람다 연산자(->)를 기준으로 오른쪽에 람다 함수를 정의한다. 람다 연산자(->)는 람다 함수로부터 파라미터를 분리해서 표현하는 역할을 한다.

여기서 알아두어야 할 점은 람다 함수 본체에서 return 키워드를 사용하지 않는다 는 점이다. return 키워드를 사용할 순 있으나, 그럴 경우 반드시 중괄호{} 를 사용해야 한다. return 키워드를 사용하지 않는다면 중괄호를 사용하지 않아도 무방하다.

1
2
3
(int a, int b) -> { return a>b ? a:b; } // 이것도 맞고,
(int a, int b) -> a>b ? a:b // 이것도 맞다.
(a, b) -> a>b ? a:b // 심지어 이것도 맞다.

오히려 헷갈릴 수 있기 때문에 중괄호도, 추론가능한 파라미터의 데이터 타입, return 키워드도 생략하는게 더 편하지 않을까 생각한다.

람다식에서 이렇게 파라미터의 데이터 타입을 생략하더라도 자바 컴파일러가 데이터 타입을 추론가능한 이유는 인터페이스를 통해서 추론이 가능하기 때문이다.


람다는 함수임에도 불구하고 메서드 이름과 타입을 작성하지 않는다. 그래서 람다를, 식별자(identifier)없이 실행가능한 함수, 즉 익명 함수(anonymous function) 라고도 이야기한다.

간단히 쓰레드를 생성하는 예제를 통해 람다가 어떻게 쓰이는지 알아보자. 하나는 람다를 적용하지 않았고, 다른 하나는 람다를 적용했다.

without Lambda

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class lambdaExam_01 {
public static void main(String[] args) {

// without Lambda
new Thread(new Runnable() {

@Override
public void run() {
System.out.println("Hello Lambda!");
}

}).start();

}
}

with Lambda

1
2
3
4
5
6
7
8
9
public class lambdaExam_02 {
public static void main(String[] args) {

// with Lambda
new Thread( () -> System.out.println("Hello Lambda!")
).start();

}
}
1
hello lambda!

둘 다 정확히 같은 결과를 출력한다. 람다를 사용해서 코드가 더 간결해진걸 확인할 수 있다. 여기서 확인해야할 점은 단지 람다식 덕분에 코드가 짧아졌다는 것뿐만이 아니다. 쓰레드를 생성하기 위해서는 쓰레드가 상속받는 Runnable 인터페이스의 run()을 구현해야 하는데, 개발자가 직접 명시하지 않은 run()를 어떻게 알고 자바컴파일러는 run() 을 구현하여 쓰레드를 생성할 수 있었을까?

함수형 인터페이스

이 비밀을 알기 위해서 Runnable 인터페이스를 열어보았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}

추상메서드가 run() 하나만 존재하는걸 알 수 있다.
그리고 이 Runnable 인터페이스위에 @FunctionalInterface가 작성되어있는데, 이것도 열어보았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package java.lang;

import java.lang.annotation.*;

/**
* ( 중략 )
*
* Conceptually, a functional interface has exactly one abstract
* method. Since {@linkplain java.lang.reflect.Method#isDefault()
* default methods} have an implementation, they are not abstract.
*
* ( 중략 )
*
* <p>Note that instances of functional interfaces can be created with
* lambda expressions, method references, or constructor references.
*
* ( 중략 )
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FunctionalInterface {}

다소 길어서 중간 부분을 좀 생략했는데, Functional Interface(함수형 프로그래밍)에 대한 설명을 해놓은 것 같다. 함수형 인터페이스는 하나의 추상 메서드를 갖는 인터페이스라고 정의해놓았다. 그리고 이 함수형 인터페이스의 인스턴스들은 람다식으로도 표현될 수 있다고 설명한다.

즉, 람다 예제 코드에서 Runnable 인터페이스를 사용할 수 있었던 이유는 Runnable 인터페이스가 추상 메서드를 하나만 갖고있는 함수형 인터페이스였기 때문이다. 그리고 추상 메서드가 하나만 있었기 때문에 람다식과 1:1로 맵핑되어 개발자가 run()를 직접 선언하지 않아도 함수 구현체를 run()으로 구현했음을 컴파일러가 인식한 것이다.


이번엔 직접 함수형 인터페이스를 만들어서 람다를 구현해보았다.

이 코드에 대한 실행은 아래 링크를 통해 확인 가능하다.

Repl에서 실행하기