람다와 Stream 예제로 이해하기

Java에서 지원하는 람다와 스트림은 코드를 더 간결하고 유지보수하기 좋은 코드를 작성하는 데 큰 도움이 됩니다. 이번 글에서는 람다와 Stream 예제 코드를 통해 기본적인 구조를 파악해보고 기본적인 설명과 함께 이들을 활용하여 코드를 어떻게 더 간결하고 효율적으로 작성할 수 있는지 살펴보겠습니다. 람다가 무엇이고, 어떻게 메서드의 인자로 전달되며, Stream은 어떻게 데이터를 다루는 데 도움이 되는지 실용적인 예제 코드를 통해 자세히 알아봅시다.

람다

람다를 메서드의 인자로 전달하는 것은 익명 함수를 전달하는 것과 같습니다. 동작을 파라미터화 하는 일이 가능해지는 겁니다. 자바 8 버전 이전에는 메서드를 가진 익명 클래스를 인스턴스로 만들어 전달하는 방법으로 비슷한 효과를 낼 수 있었습니다. 하지만 이제는 람다 덕분에 인터페이스를 만들고 구현체의 인스턴스를 생성하는 불편하고 반복적인 작업을 하지 않아도 됩니다.

하나의 동작을 전달하는 일을 쉽게 하는 것이 목적이므로 람다는 하나의 추상 메서드를 선언한 인터페이스에만 적용할 수 있다는 특징이 있습니다. 이를 통해 Java 에서도 부작용이 없고 예측 가능한 코드를 작성하는 것을 목적으로 하는 함수형 프로그래밍이 가능해집니다. 하나의 추상 메서드를 선언한 인터페이스를 함수형 인터페이스라고 부르며 대표적으로 Runnable, Supplier<R>, Consummer<T>, Predicate<T>, Funcation<T, R> 등이 제공되고 있습니다.

Functional InterfaceFunction DescriptorParameter TypeReturn Type
Runable() -> voidvoidvoid
Supplier<R>() -> RvoidR
Consummer<T>(T) -> voidTvoid
Predicate<T>(T) -> booleanTboolean
Function<T, R>(T) -> RTR
기본적인 함수형 인터페이스

람다 예제

처음에 start를 출력하고 마지막에 end를 출력하는 메서드가 있습니다. 그런데 startend 중간에 문자열 출력 동작을 인자로 받아서 때에 따라 다른 출력을 할 수 있게 하려면 어떻게 해야 할까요? 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

스스로 경험하며 얻은 깨달음을 공유하기 좋아하며, 세상이 필요로 하는 코드를 작성하기 위해 노력하는 개발자입니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다