Java에서 지원하는 람다와 스트림은 코드를 더 간결하고 유지보수하기 좋은 코드를 작성하는 데 큰 도움이 됩니다. 이번 글에서는 람다와 Stream 예제 코드를 통해 기본적인 구조를 파악해보고 기본적인 설명과 함께 이들을 활용하여 코드를 어떻게 더 간결하고 효율적으로 작성할 수 있는지 살펴보겠습니다. 람다가 무엇이고, 어떻게 메서드의 인자로 전달되며, Stream은 어떻게 데이터를 다루는 데 도움이 되는지 실용적인 예제 코드를 통해 자세히 알아봅시다.
람다
람다를 메서드의 인자로 전달하는 것은 익명 함수를 전달하는 것과 같습니다. 동작을 파라미터화 하는 일이 가능해지는 겁니다. 자바 8 버전 이전에는 메서드를 가진 익명 클래스를 인스턴스로 만들어 전달하는 방법으로 비슷한 효과를 낼 수 있었습니다. 하지만 이제는 람다 덕분에 인터페이스를 만들고 구현체의 인스턴스를 생성하는 불편하고 반복적인 작업을 하지 않아도 됩니다.
하나의 동작을 전달하는 일을 쉽게 하는 것이 목적이므로 람다는 하나의 추상 메서드를 선언한 인터페이스에만 적용할 수 있다는 특징이 있습니다. 이를 통해 Java 에서도 부작용이 없고 예측 가능한 코드를 작성하는 것을 목적으로 하는 함수형 프로그래밍이 가능해집니다. 하나의 추상 메서드를 선언한 인터페이스를 함수형 인터페이스라고 부르며 대표적으로 Runnable
, Supplier<R>
, Consummer<T>
, Predicate<T>
, Funcation<T, R>
등이 제공되고 있습니다.
Functional Interface | Function Descriptor | Parameter Type | Return Type |
---|---|---|---|
Runable | () -> void | void | void |
Supplier<R> | () -> R | void | R |
Consummer<T> | (T) -> void | T | void |
Predicate<T> | (T) -> boolean | T | boolean |
Function<T, R> | (T) -> R | T | R |
람다 예제
처음에 start
를 출력하고 마지막에 end
를 출력하는 메서드가 있습니다. 그런데 start
와 end
중간에 문자열 출력 동작을 인자로 받아서 때에 따라 다른 출력을 할 수 있게 하려면 어떻게 해야 할까요? print
메서드를 정의한 Printer
인터페이스가 있을 때 우리는 아래와 같이 run
메서드를 작성해볼 수 있습니다.
@Test
void putAnonymousTest() {
run(new Printer() {
@Override
public void print() {
System.out.println("Anonymous Print");
}
});
}
@Test
void putLambdaTest() {
run(() -> System.out.println("Lambda Print"));
}
private void run(Printer a) {
System.out.println("start");
a.print();
System.out.println("end");
}
interface Printer {
void print();
}
Printer
인터페이스는 print
추상 메서드 하나만 선언하고 있기 때문에 함수형 인터페이스가 되기 위한 조건을 만족합니다. 즉, Printer
인터페이스는 함수형 인터페이스이기 때문에 람다를 적용할 수 있습니다. print
메서드는 파라미터가 void
, 리턴 타입이 void
입니다. 어디서 본 것 같지 않나요? 위에서 Runnable 이라는 함수형 인터페이스가 제공된다고 했었습니다. run
메서드의 파라미터 타입을 Printer
대신 Runnable
로 대체할 수 있습니다.
람다를 사용하면 얼마나 코드가 간결해지는지 보여드리기 위해 putAnonymousTest
, putLambdaTest
두 개의 메서드를 만들었습니다. 두 메서드는 동일한 동작을 합니다. 참고로 putAnonymousTest
메서드의 new Printer()
부분을 new Runnable()
로 대체하는 것도 가능합니다. Runnable
인터페이스를 사용할 경우 구현해야 하는 추상 메서드의 이름은 print
가 아닌 run
이 됩니다.
putAnonymousTest
메서드를 보면 run
메서드를 실행할 때 익명 클래스를 생성해서 인자로 전달하고 있고 putLambdaTest
메서드에서는 run
메서드를 실행할 때 람다를 인자로 전달하고 있습니다. 불필요한 코드가 제거된 것이 느껴지시나요?
Stream
스트림은 데이터가 일렬로 연결된 파이프를 통과하는 모습을 상상하면 이해하기 쉽습니다. 파이프는 여러 종류가 있습니다. 각 파이프 내부에서는 특정한 작업 수행 후 다음 파이프로 작업 완료된 데이터를 넘겨주거나(중간 연산) 일을 마무리(최종 연산)합니다. 스트림을 사용하면 컬렉션(리스트, 맵 등)의 요소들을 함수형 프로그래밍으로 처리하여 간결하면서 예상하기 쉬운 코드를 작성할 수 있게 됩니다. 예제 코드를 보면서 감을 잡아봅시다.
Stream 예제
실무에서 빈번하게 사용되는 스트림 연산을 사용하여 간단한 문제를 해결해 보겠습니다.
어떤 문자열 리스트에서 특정 조건에 해당하는 문자열을 골라낸 뒤 소문자로 변환한 결과를 리스트로 만들어 반환하고 싶다면 어떻게 해야 할까요?
아래와 같이 코드를 작성해볼 수 있습니다. 생략할 수 있는 부분도 있지만 이해를 돕기 위해 생략하지 않았습니다.
@Test
void streamTest() {
List<String> list = Lists.newArrayList("ABC", "BCD", "CDE");
List<String> result = list.stream()
.filter(str -> str.contains("D"))
.map(filteredStr -> {
return filteredStr.toLowerCase();
})
.collect(Collectors.toList());
Assertions.assertThat(result)
.containsExactly("bcd", "cde");
}
중간 연산으로 filter
를 사용해서 String 리스트에서 “D”를 포함하는 String을 필터링하고 map
을 사용해서 소문자로 변환하는 작업을 수행할 수 있습니다. 그리고 최종 연산인 collect
를 사용해 결과를 리스트에 담아서 반환합니다.
filter 연산은 특정 조건으로 필터링, map 연산은 다른 값으로 변환할 때 사용하며, collect 연산은 중간에서 필터링 + 변환한 값을 모아서 Collection으로 만들어 반환해준다고 생각하시면 쉽게 이해하실 수 있습니다.
위 예제에서 사용한 연산들 외에도 다양한 연산이 제공되고 있으니 Stream의 중간 연산, 최종 연산 API에 대해서 자료를 찾아보고 필요한 내용을 학습하여 적용해보시길 바랍니다.
참고할 만한 사이트 click!! 👉 https://java-8-tips.readthedocs.io/en/stable/streamsapi.html