Lambda 표현식은 자바에서 제공하는 익명 클래스를 대체하는 문법입니다. 익명 클래스의 가독성을 개선하기 위해 출시되었으나, Labmda 표현식 자체도 처음 접할 경우 읽기 힘든 부분이 존재합니다. if, while 등 다른 직관적인 키워드들에 비해 사전에 알아야 할 지식도 있습니다. Lambda 표현식을 이해하기 위해서는 함수형 프로그래밍, 익명 클래스, 익명 함수, Functional Interface에 대한 개념이 필요합니다. 현업에서 많이 사용되고 있는 키워드인 만큼 가독성있고 일관성있는 코드를 위해서 이 글을 통해 Lambda 표현식의 개요와 활용에 대해 이해할 수 있기를 기대합니다.
자바 8 이전에는 함수형 프로그래밍을 구현하기 위해 익명 클래스(Anonymous Class)를 사용했습니다. 익명 클래스는 많은 보일러 플레이트 코드(여러 가지 상황에서 거의 또는 전혀 변경하지 않고 재사용할 수 있는 코드)로 인해 가독성이 떨어지는 단점이 있었습니다. 이러한 문제를 개선하기 위해 자바 8에서 Lambda 표현식이 도입되었으며, 이를 통해 함수형 프로그래밍을 보다 직관적이고 간결하게 구현할 수 있게 되었습니다. Lambda 표현식은 하나의 추상 메서드만을 가지는 Funtional Interface에만 사용할 수 있습니다.
(int x, int y) -> x + y
(x, y) -> x + y
x -> x + 1
(함수의 입력 매개변수) -> 바디Runnable 인터페이스 익명 클래스 사용
// 익명 클래스 사용
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Hello, world!");
}
};
Runnable 인터페이스 Lambda 표현식으로 개선
// Lambda 표현식 사용, 간결하고 가독성이 높아짐
Runnable r = () -> System.out.println("Hello, world!");
Comparator 인터페이스 익명 클래스 사용
// 익명 클래스 사용
Comparator<String> comp = new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.compareTo(s2);
}
};
Comparator 인터페이스 Lambda 표현식으로 개선
// Lambda 표현식 사용, 간결하고 가독성이 높아짐
Comparator<String> comp = (s1, s2) -> s1.compareTo(s2);
함수형 프로그래밍은 위키백과 기준으로 자료 처리를 수학적 함수의 계산으로 취급하고 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임의 하나라는 의미를 가지고 있습니다. 주요 특징으로는 일급 함수, 순수 함수, 불변성이 있습니다.
Funtional Interface는 추상 메서드를 하나만 가지는 인터페이스를 말합니다. 이러한 인터페이스는 단 하나의 추상 메서드만을 가지기 때문에 람다 표현식에 사용할 수 있습니다. @FuntionalInterface 어노테이션을 사용하여 해당 인터페이스가 하나의 추상 메서드를 가지는 것이 맞는지 컴파일 시점에 검증할 수 있습니다. 주로 사용되는 Funtional Interface로는 아래와 같은 것들이 있습니다.
Runnable r = () -> System.out.println("Hello, world!"); // 스레드를 실행할 때 수행할 작업을 반환
Predicate<String> length5 = s -> s.length() > 5; // 두 개의 객체를 비교하고 boolean을 반환
Consumer<String> print = s -> System.out.println(s); // 특정 작업을 수행하고 결과를 반환하지 않음
Function<String, Integer> toLength = s -> s.length(); // 인수를 다른 객체로 변환
Supplier<Double> randomValue = () -> Math.random(); // 인수를 받지 않고 값을 반환
UnaryOperator<String> toUpperCase = s -> s.toUpperCase(); // 인수를 동일한 타입의 다른 인수로 변환
BinaryOperator<Integer> sum = (a, b) -> a + b; // 두 인수를 동일한 타입의 다른 인수로 결합
BiFunction<String, String, Integer> compare = (a, b) -> a.compareTo(b); // 두 인수를 다른 타입의 객체로 변환
BiConsumer<String, String> printBoth = (a, b) -> System.out.println(a + " " + b); // 두 인수를 받아서 특정 작업을 수행
Stream API는 자바 8에서 도입된 기능으로, Lambda 표현식과 함께 사용되며 List, Set 같은 컬렉션 데이터를 효율적이고 직관적으로 처리할 수 있는 방법을 제공합니다. 컬렉션 데이터를 스트림 객체로 변환하고, 다양한 중개 연산(filter, map, sorted 등)과 종단 연산(collect, forEach, reduce 등)을 사용해 데이터를 처리하는데 도움을 줍니다. 또한 parallelStream 메서드를 통해 병렬 처리도 가능하지만, 스레드 안정성 문제와 성능이 오히려 안 좋아질 수도 있는 문제가 있기 때문에 신중히 사용하여야 합니다.
filter(): 조건에 맞는 요소만 반환
List<String> filteredList = list.stream()
.filter(s -> s.startsWith("a")) // 리스트에서 'a'로 시작하는 문자열만 필터링
.collect(Collectors.toList());
map(): 각 요소에 주어진 함수의 결과를 적용해서 반환
List<String> upperCaseList = list.stream()
.map(String::toUpperCase) // 스트림의 요소들을 대문자로 변환
.collect(Collectors.toList());
sorted(): 요소를 정렬한 결과를 반환
List<String> sortedList = list.stream()
.sorted()
.collect(Collectors.toList());
limit(): 스트림의 크기를 지정한 수로 제한해서 반환
List<String> limitedList = list.stream()
.limit(2)
.collect(Collectors.toList());
collect(): 요소를 컬렉션으로 반환
List<String> collectedList = list.stream()
.collect(Collectors.toList());
forEach(): 각 요소에 대해 주어진 동작을 수행
list.stream()
.forEach(System.out::println);
reduce(): 요소를 결합하여 하나의 값으로 만듦
int sum = numbers.stream()
.reduce(0, Integer::sum);
count(): 요소의 개수를 반환
long count = list.stream()
.count();
Lambda 표현식은 Functional Interface에만 사용가능한, 익명 클래스보다 깔끔한 코드를 작성하기 위해 나온 문법입니다. 문법이 생소해서 이해하기 힘든 점은 Lambda 표현식을 처음 접했을 때의 단점이긴 하지만, 여러 예시들을 통해 보았듯이 잘 활용할 경우 가독성과 일관성, 유지보수성을 높일 수 있습니다. 서비스가 점점 발전하듯이 개발 언어와 프레임워크도 점점 발전합니다. 발전된 유용한 기술들을 잘 활용하는 것은 서비스의 성능과 안정성 뿐만 아니라 유지보수의 수고를 덜어주는데 중요한 역할을 할 것입니다. 이제 Lambda 표현식에 더 가까워졌다면, 익명 클래스를 만났을 때 Lamda로 변환해보시기를 추천드립니다.