[Java] 자바 람다식과 함수형 인터페이스(Functional Interface) - (1)
kindof
·2021. 11. 2. 23:29
1. 들어가면서 - 람다식이란?
이번 시간에는 크게 네 가지 주제, [1] 람다식이란 무엇인가 [2] 어떻게 작성하는가 [3] 어떻게 동작하는가 [4] 어떻게 활용하는가에 대해 설명해보겠습니다.
보통 객체지향적 프로그래밍을 한다는 것은 프로그램 안에서 상호작용하는 객체들을 중심으로 객체가 가지는 값이나 행위들로 프로그램을 끌고 나가는 것을 말합니다.
반면, 함수형 프로그래밍은 프로그램을 상태값을 지니지 않는 함수값들의 연속으로 생각할 수 있게 해주는 방법을 말하죠.
이러한 관점에서 바라볼 때, JDK1.8에서 도입된 람다식(Lambda Expression)은 메서드를 하나의 식(expression)으로 표현할 수 있게 하여 자바가 객체지향언어인 동시에 함수형 언어의 속성을 갖게 해주었습니다.
뒤에서도 살펴보겠지만, 메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지고 람다식은 '메서드'라는 이름 대신에 '익명 함수(anonymous function)' 라는 '함수'로써의 역할을 할 수 있게 되기 때문입니다.
람다식에 대한 감을 잡아보기 위해 간단한 예시 하나를 보겠습니다.
int[] arr = new int[5];
Arrays.setAll(arr, (i) -> (int)(Math.random() * 5) + 1);
위 코드에서 '() -> (int)(Math.random() * 5) + 1'을 람다식이라고 볼 수 있는데요.
만약 람다식이 없었다면 (int)(Math.random() * 5) + 1) 이라는 작업을 처리하기 위해 하나의 메서드를 생성했어야 했을 겁니다. 그리고 하나의 메서드는 어떤 인터페이스나 클래스 안에 정의가 되어있었어야겠죠.
바로 이 부분을 생각해봤을 때, 람다식이 없던 과거에는
1) 동일한 작업을 하는 메서드를 만들어야 했을 것이고, 모든 메서드는 어떤 클래스에 포함되어야 하므로
2) 클래스도 새로 만들어야 하고,
3) 정적 메서드가 아니라면 객체도 생성해야만 비로소 이 메서드를 호출할 수 있었을 겁니다.
하지만 람다식은 이 모든 과정없이 오직 람다식 자체만으로도 메서드의 역할을 대신할 수 있게 해준 것입니다.
2. 람다식 작성하기
아래에서는 람다식을 어떻게 작성하는지 말 그대로 '방법'에 초점을 맞춰서 설명하겠습니다. 어떻게 이러한 방식으로 람다식이 동작하는가는 좀 더 아래 '함수형 인터페이스'를 설명하면서 이야기하겠습니다!
람다식은 익명 함수답게 메서드에서 이름과 반환타입을 제거하고 매개변수 선언부와 몸통{ } 사이에 '->'를 추가합니다.
예를 들어 두 값 중에서 큰 값을 반환하는 메서드 max를 람다식으로 변환하면 아래와 같이 됩니다.
int max(int a, int b){
return a > b ? a : b;
}
// Lambda Expression
(int a, int b) -> {
return a > b ? a : b;
}
그리고 반환값이 있는 메서드의 경우, return문 대신 식(expression)으로 대신할 수 있는데요. 식의 연산결과가 자동적으로 반환값이 됩니다. 아래 코드를 보겠습니다.
// return 존재
(int a, int b) -> {return a > b ? a : b;}
// return 생략
(int a, int b) -> a > b ? a : b
또한 람다식에 선언된 매개변수의 타입은 추론이 가능한 경우에는 생략이 가능하고, 대부분의 경우에 생략이 가능합니다. 람다식에 반환타입이 없는 이유도 항상 추론이 가능하기 때문입니다.
// 매개변수 타입 존재
(int a, int b) -> a > b ? a : b
// 매개변수 타입 생략
(a, b) -> a > b ? a : b
아래 메서드는 배열 내 원소들의 합을 리턴해주는 메서드입니다.
int sumArr(int[] arr){
int sum = 0;
for(int i : arr){
sum += i;
}
return sum;
}
이 메서드를 람다식으로 고치면 아래와 같이 쓸 수 있습니다.
(int[] arr) -> {
int sum = 0;
for(int i : arr){
sum += i;
}
return sum;
}
람다식에서도 괄호{} 안의 코드가 한 줄밖에 없을 때는 괄호를 생략할 수 있지만, 위의 람다식처럼 여러 라인의 내용이 존재하면 괄호를 생략할 수 없습니다.
람다식을 실제로 사용하는 예시를 한 번 보겠습니다.
위 코드는 유저가 댓글을 남긴 게시물들을 모두 조회하는 컨트롤러의 코드 중 일부입니다. User-Comment는 OneToMany 관계이고, Post-Comment 역시 OneToMany 관계에 있기 때문에 쿼리를 두 번 사용해야 했는데요.
먼저 유저의 아이디를 가지고 유저가 남긴 Comment들을 distinct하게 추출해서 userCommentPostId 리스트에 담았습니다.
그리고 이 리스트 안의 Id를 순회하기 위해 stream()을 쓰고 map 안에 람다식을 사용했습니다. 람다식 안에 Id를 매개변수로 넘기고, Id를 가지고 조회한 포스트들을 모두 리턴받은 후 마지막에 다시 리스트 형태로 collect()한 것입니다.
여기까지 정말 기본적인 람다식의 개념과 람다식의 작성에 대해 알아봤습니다.
이제부터 람다식이 어떻게 동작하는지, 함수형 인터페이스와 좀 더 깊은 내용들을 공부하면서 알아보도록 하겠습니다.
3. 함수형 인터페이스(Functional Interface)
💡 처음에 읽으면 헷갈리실 수 있는데, 천천히 읽어보시고 아래 예제를 보시면 이해가 되실 겁니다😅
갑자기 '함수형 인터페이스'라는 용어가 등장했습니다. 함수형 인터페이스가 무엇인지 아래 질문에 대한 답을 찾아가보면서 이야기해보겠습니다.
자바에서 모든 메서드는 클래스 내에 포함되어야 합니다. 그렇다면 람다식은 어떤 클래스에 포함되는 것일까요?
지금까지 람다식이 메서드와 동등한 것처럼 설명했지만, 사실 람다식은 익명 클래스의 객체와 동등합니다.
// 람다식
(int a, int b) -> a > b ? a : b
// 익명 클래스
new Object(){
int max(int a, int b){
return a > b ? a : b;
}
}
위의 코드에서 메서드 이름 max는 사실 아무런 의미가 없습니다. 그렇다면 람다식으로 정의된 익명 객체의 메서드를 어떻게 호출할 수 있을까요? 객체의 메서드는 참조 변수가 있어야 호출할 수 있으므로, 이 익명 객체의 주소를 f라는 참조변수에 저장해보겠습니다.
타입 f = (int a, int b) -> a > b ? a : b;
그런데 이 때, 참조변수의 타입은 무엇으로 해야 할까요?
참조형은 클래스 또는 인터페이스가 가능할텐데, 람다식과 동등한 메서드가 정의되어 있는 것이어야 합니다. 그래야만 참조변수로 익명 객체(람다식)의 메서드를 호출할 수 있기 때문이죠.
예를 들어 아래와 같이 max()라는 메서드가 정의된 MyFunction인터페이스가 정의되어 있다고 해보겠습니다.
interface MyFunction{
public abstract int max(int a, int b);
}
그러면 이 인터페이스를 구현한 익명 클래스의 객체는 아래처럼 MyFunction이라는 타입으로 생성할 수 있게 됩니다.
MyFunction f = new MyFunction(){
public int max(int a, int b){
return a > b ? a : b;
}
};
int big = f.max(5, 3); // 익명 객체의 메서드를 호출
MyFunction인터페이스에 정의된 메서드 max()는 람다식 '(int a, int b) -> a > b ? a : b'과 메서드의 선언부가 일치합니다. 그래서 위 코드의 익명 객체를 람다식으로 아래와 같이 대체할 수 있습니다.
MyFunction f = (int a, int b) -> a > b ? a : b; // 익명 객체를 람다식으로 대체
int big = f.max(5, 3); // 익명 객체의 메서드를 호출
💡 이처럼 MyFunction 인터페이스를 구현한 익명 객체를 람다식으로 대체가 가능한 이유는 : 람다식도 실제로는 익명 객체이고, MyFunction 인터페이스를 구현한 익명 객체의 메서드 max()와 람다식의 매개변수의 타입과 개수 그리고 반환값이 일치하기 때문입니다.
따라서, 하나의 메서드가 선언된 인터페이스를 정의해서 람다식을 다루는 것은 기존의 자바의 규칙들을 어기지 않으면서도 효율적이고 자연스러운 방법이 되는 것이죠!
그래서 자바 진영은 인터페이스를 통해 람다식을 다루기로 약속했고, 람다식을 다루기 위한 인터페이스를 '함수형 인터페이스(functional interface)'라고 부르게 됐습니다.
@FunctionalInterface
interface MyFunction{ // 함수형 인터페이스 MyFunction을 정의
public abstract int max(int a, int b);
}
위 코드에서 @FunctionalInterface 어노테이션을 붙이게 되면 컴파일러가 함수형 인터페이스를 올바르게 정의했는지 확인해줍니다.
한편, 함수형 인터페이스에는 오직 하나의 추상 메서드만 정의되어 있어야 하는데, 그래야 람다식과 인터페이스의 메서드가 1:1로 연결될 수 있기 때문입니다. 반면에 static 메서드와 default 메서드의 개수에는 제약이 없습니다.
예시 하나를 통해 위에서 했던 이야기를 정리해보겠습니다.
"기존에 아래와 같이 인터페이스의 메서드 하나를 구현하는데도 복잡한 과정이 필요했습니다.
List<String> list = Arrays.asList("abc", "aaa", "bbb", "ddd", "aaa");
Collections.sort(list, new Comparator<String>(){
public int compare(String s1, String s2){
return s2.compareTo(s1);
}
});
그런데 이제 람다식으로 아래와 같이 간단히 처리할 수 있게 되었고,
List<String> list = Arrays.asList("abc", "aaa", "bbb", "ddd", "aaa");
Collections.sort(list, (s1, s2) -> s2.compareTo(s1));
이것이 가능한 이유는 위에서 설명했던 것처럼 Comparator<T> 인터페이스 자체가 '함수형 인터페이스'로 정의되어 있었고, 이미 int compare() 메서드의 파라미터 개수와 반환 타입이 일치하기 때문이었습니다."
이 부분에 대해 약간 이해가 잘 안 되시거나, 궁금하신 분이 계시다면 방금 작성했던 코드를 그대로 입력해보고 Comparator<T> 인터페이스를 직접 확인해보시면서 공부하면 도움이 될 것 같습니다.
4. 나가면서
이렇게 이번 시간에는 람다식의 개념과 람다식의 작성 방법, 그리고 람다식을 사용하는 데 도구가 되는 함수형 인터페이스에 대해 공부했습니다.
자바 람다식과 함수형 인터페이스 2편 포스팅에서는 함수형 인터페이스와 관련된 이야기를 좀 더 해보겠습니다.
감사합니다.
'Java & Kotlin' 카테고리의 다른 글
[Java] 제네릭(Generics)에 대해 생각해보기 (0) | 2021.11.23 |
---|---|
[JAVA] 자바 hashCode() (0) | 2021.11.20 |
[자바/JAVA] 생성자(Constructor) (0) | 2021.08.02 |
[자바/Java] 오버로딩(Overloading) (0) | 2021.08.02 |
[JAVA] 기본형 매개변수와 참조형 매개변수 (0) | 2021.07.11 |