며칠 전 학교수업에서 람다를 배우게 되었는데, 평소 프로젝트를 할 때도 자주 사용했었지만 정작 무엇인
지 정확히 알고 쓰지 못하는 것 같아 오늘은 람다가 무엇인지, Lambda가 도입하게 된 함수형 인터페이스란 무엇인지 알아보겠다.
익명 함수의 한 형태로, 메소드를 하나의 식으로 표현하는 방법
람다 표현식은 객체 지향 프로그래밍 언어인 Java에 함수형 프로그래밍 패러다임의 특징을 가져오게 한 중요한 부분이다.
코드를 더욱 간결하게 만들어주고, 메소드를 데이터처럼 전달할 수 있게 해준다.
원래 함수를 작성하기 위해서는
반환타입 메서드이름 (매개변수) {
문장들;
}
이러한 형태로 작성해야했다. 하지만 람다는 이를 더 간단하게 만들었다.
기본적인 Lambda 표현식의 문법은 다음과 같다.
(매개변수) -> { 본문; }
람다식은 '익명 함수'답게 함수에서 반환타입을 제거하고, 매개변수 선언부와 몸통 {} 사이에 -> 를 추가한다.
각 부분이 무엇을 의미하는지 알아보자.
기본 구조를 알아봤으니 간단한 람다식을 사용한 예제를 살펴보자.
일단은 람다식을 사용하지 않은 방법이다.
List<String> items = Arrays.asList("Apple", "Banana", "Cherry", "Orange");
for (String item : itmes) {
System.out.println(item);
}
위 코드는 for-each문을 통해 리스트를 순회하여 출력하는 간단한 예제이다. 이제 이 코드에 람다식을 적용해보자.
List<String> items = Arrays.asList("Apple", "Banana", "Cherry", "Orange");
items.forEach(item -> System.out.println(item));
이 두 코드를 비교해보면 확실히 람다식을 사용한 경우 코드 길이가 더 짧아지고, 의도가 더 명확하게 보인다는 것을 알 수 있다.
특히 Stream과 Lambda가 결합될 때, 코드의 가독성을 크게 증가시킬 수 있다.
Stream과 결합한 간단한 예제도 살펴보자.
List<String> names = Arrays.List("Frog", "Lion", "Tiger", "Rabbit", "Riger");
List<String> filteredNames = new ArrayList<>();
for (String name : name) {
if (name.startsWith("R")) {
String uppercaseName = name.toUppercase();
filteredNames.add(uppercaseName);
}
}
System.out.println(filteredNames);
이 예제는 'R'로 시작하는 동물이름만 필터링하고, 그 결과를 리스트에 저장하고 출력하는 코드이다. 이 코드 역시 for-each문을 사용하고 있다.
이제 Lambda와 Stream을 이 코드에 적용시켜보자.
List<String> names = Arrays.List("Frog", "Lion", "Tiger", "Rabbit", "Riger");
List<String> filteredNames = names.stream()
.filter(name -> name.startsWith("S"))
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(filteredNames);
두 예제를 살펴보면 사실 코드의 길이는 차이가 없다.
하지만 Lambda와 Stream을 사용한 경우, 각 단계의 목적이 명확하게 드러나기 때문에 코드를 더 읽기 쉽고, 유지보수가 용이해진다.
함수형 인터페이스란 과연 무엇일까?
new Object() {
int mIn(int a, int b) {
return a < b ? a : b;
}
}
(int a, int b) -> a < b ? a : b
우리는 위에서 봤다시피 이 두 코드는 같은 역할을 한다.
그렇다면 아래에 있는 람다식으로 정의된 익명 객체의 메서드를 어떻게 호출할 수 있을까?
(타입) f = (int a, int b) -> a < b ? a : b;
이렇게 참조변수를 선언하여 호출할 수 있을 것이다.
그러면, 참조변수 f는 어떤 것이여야 할까?
바로 람다식과 동등한 메서드가 정의되어 있는 클래스나 인터페이스여야 한다.
예를 들어 위의 min() 메서드가 정의된 인터페이스가 있다고 해보자.
interface FrogInterface {
public abstact int max(int a, int b);
}
그러면 이 인터페이스를 구현한 익명 클래스의 객체를 생성할 수 있다.
FrogInterface f = new FrogInterface() {
public int min(int a, int b) {
return a < b ? a : b;
}
}
int minNumber = f.min(5, 3);
우리는 이제 min()을 람다식으로 대체할 수 있다는 것을 알고 있다. 적용해본다면
FrogInterface f = (int a, int b) -> a < b ? a : b;
int minNumber = f.min(5, 3);
이처럼 인터페이스를 구현한 익명 객체를 람다식으로 대체가 가능한 이유는, 람다식도 실제로는 익명 객체리고, 인터페이스를 구현한 익명 객체의 메서드 min()과 람다식의 매개변수의 타입과 개수 그리고 반환값이 모두 일치했기 때문이다.
하나의 메서드가 선언된 인터페이스를 정의해서 람다식을 다루는 것은 매우 자연스럽다.
그래서 인터페이스를 통해 람다식을 다루기로 결정이 되었으며, 람다식을 다루기 위한 인터페이스를 '함수형 인터페이스'라고 명명하였다.
지금까지 람다 표현식의 이점을 살펴보았지만, 반대로 한계점도 존재한다. 두 가지를 한 번에 살펴보자.
람다 표현식은 확실한 이점이 있지만, 사용할 때 상황에 맞게 사용을 하여야 그 이점을 극대화할 수 있다.
즉, 무분별한 람다 표현식 사용은 코드의 가독성을 더욱 떨어뜨릴 수 있다.