Library

람다 표현식(Lambda Expression)

Andrew's Akashic Records 2024. 10. 21. 11:36
728x90

람다 표현식이란?

 람다 표현식(Lambda Expression)은 익명 함수(anonymous function)라고 할 수 있습니다. 자바에서는 메서드를 객체처럼 취급할 수 없기 때문에, 함수형 인터페이스라는 단일 추상 메서드를 갖는 인터페이스를 정의한 후 그 인터페이스를 구현하는 방식으로 함수형 프로그래밍을 지원합니다. 이때 메서드 구현을 간단하게 표현할 수 있도록 도와주는 것이 바로 람다 표현식입니다.

 

 간단히 말하면, 람다 표현식은 이름이 없는 함수로, 간단한 식이나 메서드를 더욱 짧고 간결하게 표현할 수 있게 해주는 문법입니다. 전통적으로 메서드를 정의하는 방식보다 훨씬 간결하게 코드를 작성할 수 있습니다.

람다(Lambda)

람다(Lambda)의 의미

 람다(Lambda)라는 용어는 람다 계산법(Lambda Calculus)에서 유래했습니다. 람다 계산법은 수학자 알론조 처치(Alonzo Church)가 1930년대에 제안한 수학적 모델로, 함수 정의와 함수 적용을 위한 형식 체계입니다. 이는 현대 함수형 프로그래밍 언어의 기초가 된 개념입니다. 여기서 "람다"는 익명 함수, 즉 이름 없는 함수를 표현하는 데 사용됩니다.

예: 수학적 람다 계산법에서의 표현

람다 계산법에서 함수 f(x) = x + 1λx.x + 1로 표현할 수 있습니다. 여기서 λ가 바로 "람다"로, 이는 함수 정의를 의미합니다.

 

 이 수학적 개념이 프로그래밍 언어에 도입되면서 람다라는 용어가 "익명 함수"를 표현하는 문법적 장치로 사용되게 된 것입니다.

자바에서의 람다 표현식 예

 람다 표현식을 사용하면 코드를 더욱 간결하게 작성할 수 있습니다. 예를 들어, 두 정수를 더하는 메서드를 전통적인 방식으로 작성한다면 다음과 같습니다.

BinaryOperator<Integer> addition = new BinaryOperator<Integer>() {
    @Override
    public Integer apply(Integer x, Integer y) {
        return x + y;
    }
};

람다 표현식을 사용하면 이 코드를 아래와 같이 간단하게 작성할 수 있습니다.

BinaryOperator<Integer> addition = (x, y) -> x + y;

람다의 의미 요약

  • 람다는 함수형 프로그래밍의 개념에서 유래한 용어로, 익명 함수를 의미합니다.
  • 자바에서는 람다 표현식을 통해 메서드나 함수를 간결하게 정의하고, 더 쉽게 코드에서 사용할 수 있습니다.
  • 이는 코드의 간결성가독성을 높이고, 특히 스트림 API 등에서 함수형 프로그래밍을 구현하는 데 유용한 도구입니다.

람다 표현식은 이러한 수학적, 프로그래밍적 배경에서 유래하여 자바 8에 도입된 것으로, 함수형 프로그래밍 스타일을 가능하게 해줍니다.

자바에 람다 표현식이 추가된 이유

자바에 람다 표현식이 추가된 주요 이유는 다음과 같습니다:

  1. 함수형 프로그래밍 지원: 자바는 전통적으로 객체지향 언어로 널리 사용되어 왔지만, 현대의 복잡한 소프트웨어 개발에서는 함수형 프로그래밍의 유연성과 간결성이 필요해졌습니다. 특히 컬렉션(리스트, 맵 등)을 처리하는 작업에서 명령형 코딩보다 함수형 접근 방식이 더 직관적이고 간결합니다. 이를 위해 자바 8에서는 람다 표현식과 함께 스트림 API가 도입되었고, 이를 통해 함수형 프로그래밍의 장점을 활용할 수 있게 되었습니다.
  2. 코드 간결화: 람다 표현식은 익명 클래스나 반복적인 코드를 간단하게 표현할 수 있도록 도와줍니다. 예전에는 이벤트 처리나 쓰레드 실행 시 간단한 인터페이스도 긴 코드로 작성해야 했는데, 이를 단순화함으로써 개발자의 코드 작성 속도와 가독성을 높여줍니다.
  3. 병렬 처리 및 스트림 API의 사용: 자바 8에서 도입된 스트림 API는 대용량 데이터를 효과적으로 처리할 수 있는 강력한 도구입니다. 스트림 API는 기본적으로 함수형 프로그래밍 패턴에 기반하고 있으며, 람다 표현식이 이를 사용하기 위한 핵심 요소입니다. 특히 병렬 스트림(parallel stream)을 사용해 데이터를 효율적으로 병렬 처리할 수 있는 환경을 제공하는데, 이 과정에서 람다 표현식이 큰 역할을 합니다.

람다 표현식의 장점

  1. 코드 간결성: 람다 표현식은 기존의 익명 클래스보다 훨씬 간결하게 코드를 작성할 수 있습니다. 불필요한 클래스나 메서드 정의를 생략하고, 기능 자체에만 집중할 수 있도록 돕습니다.
    • 예시:
      기존 방식:
      new Thread(new Runnable() {
          @Override
          public void run() {
              System.out.println("Hello, World!");
          }
      }).start();
      람다 표현식:
      new Thread(() -> System.out.println("Hello, World!")).start();
  2. 함수형 프로그래밍 지원: 자바가 객체지향 언어로서의 특징을 유지하면서도, 함수형 프로그래밍의 강점을 결합할 수 있게 해줍니다. 이로 인해 선언형 프로그래밍이 가능해지고, 특히 스트림 API와 결합하여 데이터를 더 직관적이고 간결하게 처리할 수 있습니다.
  3. 가독성 향상: 간결한 표현 덕분에 코드의 가독성이 높아집니다. 특히 짧고 단순한 작업을 할 때 명확하게 의도를 표현할 수 있습니다.
  4. 병렬 처리의 용이성: 스트림 API와 병렬 스트림의 사용이 훨씬 쉬워졌습니다. 람다 표현식을 사용하면 병렬 처리를 포함한 복잡한 작업도 간단하게 구현할 수 있습니다.
  5. 객체지향과 함수형 프로그래밍의 혼합: 자바가 객체지향 언어의 장점을 유지하면서도 함수형 프로그래밍의 이점을 결합할 수 있게 해줍니다. 이로 인해 더 유연한 프로그래밍 패러다임을 지원합니다.

람다 표현식의 단점

  1. 디버깅 어려움: 람다 표현식은 코드가 간결해지는 장점이 있지만, 그만큼 디버깅이 어려울 수 있습니다. 특히 익명 함수로 처리되기 때문에 디버깅 시 호출 스택에 해당 람다의 위치를 정확하게 찾기 어려운 경우가 발생할 수 있습니다.
  2. 복잡한 로직에 적합하지 않음: 람다 표현식은 주로 짧고 간결한 작업에 적합하며, 복잡한 로직을 처리하는 데에는 부적합할 수 있습니다. 복잡한 로직을 람다 표현식으로 구현하려고 하면 오히려 코드의 가독성이 떨어지고 유지보수가 어려워질 수 있습니다.
  3. 함수형 인터페이스에 의존: 람다 표현식은 함수형 인터페이스에서만 사용할 수 있습니다. 즉, 자바의 람다는 완전히 자유롭게 사용할 수 있는 함수가 아니라, 반드시 특정 인터페이스의 구현체로 사용되어야 한다는 제약이 있습니다. 이로 인해 자유도가 다소 제한될 수 있습니다.
  4. 코드 가독성 저하의 가능성: 간결한 코드가 항상 가독성을 높이는 것은 아닙니다. 특히 람다 표현식이 중첩되거나 너무 길어지면 코드가 복잡해져 읽기 어렵게 될 수 있습니다.
  5. 성능 문제: 람다 표현식은 내부적으로 익명 클래스를 생성하여 사용하기 때문에, 경우에 따라서는 성능 저하를 일으킬 수 있습니다. 특히 객체를 많이 생성하거나 메모리를 많이 사용하는 작업에서는 성능에 영향을 미칠 수 있습니다.

람다 표현식은 자바의 생산성과 가독성을 높이는 데 크게 기여하지만, 복잡한 로직이나 디버깅이 어려운 상황에서는 주의가 필요합니다. 상황에 맞게 적절히 사용하는 것이 중요하며, 특히 간결하고 명확한 코드 작성을 위한 도구로 유용하게 사용할 수 있습니다.

람다 표현식 예시코드

다양한 상황에서 자주 사용되는 람다 표현식을 몇 가지 예시로 살펴보겠습니다. 각각의 예시는 자바 8 이후에 도입된 스트림 API와 함께 자주 쓰이며, 함수형 인터페이스와 람다 표현식을 활용하여 컬렉션을 처리하는 방법을 보여줍니다.

예시 1: 리스트 필터링

리스트에서 특정 조건을 만족하는 요소만 필터링하는 코드입니다.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class LambdaExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("John", "Jane", "Jack", "Doe");

        // 이름이 'J'로 시작하는 요소들만 필터링
        List<String> filteredNames = names.stream()
            .filter(name -> name.startsWith("J"))
            .collect(Collectors.toList());

        // 결과 출력
        System.out.println(filteredNames);
    }
}

코드 설명:

  • names.stream(): 리스트를 스트림으로 변환하여 함수형 프로그래밍 스타일로 처리할 수 있게 합니다.
  • .filter(name -> name.startsWith("J")): 람다 표현식을 사용하여 각 요소가 'J'로 시작하는지를 검사합니다. name.startsWith("J")는 각 이름이 'J'로 시작하는지 확인하는 조건입니다.
  • .collect(Collectors.toList()): 필터링된 결과를 리스트로 다시 수집합니다.
  • 출력 결과: [John, Jane, Jack]

예시 2: 리스트의 모든 요소 변환

리스트의 모든 요소를 대문자로 변환하는 코드입니다.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class LambdaExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("John", "Jane", "Jack", "Doe");

        // 모든 이름을 대문자로 변환
        List<String> upperCaseNames = names.stream()
            .map(name -> name.toUpperCase())
            .collect(Collectors.toList());

        // 결과 출력
        System.out.println(upperCaseNames);
    }
}

코드 설명:

  • .map(name -> name.toUpperCase()): map 메서드를 사용하여 각 요소를 변환합니다. 여기서는 name.toUpperCase()를 호출해 각 이름을 대문자로 변환합니다.
  • 출력 결과: [JOHN, JANE, JACK, DOE]

예시 3: 리스트의 요소 합계 계산

정수 리스트의 모든 요소를 더해 합계를 구하는 코드입니다.

import java.util.Arrays;
import java.util.List;

public class LambdaExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        // 모든 숫자의 합 계산
        int sum = numbers.stream()
            .reduce(0, (a, b) -> a + b);

        // 결과 출력
        System.out.println("합계: " + sum);
    }
}

코드 설명:

  • .reduce(0, (a, b) -> a + b): reduce 메서드는 스트림의 모든 요소를 하나의 값으로 결합할 때 사용됩니다. 여기서는 0에서 시작해 각 숫자를 차례대로 더해 나갑니다.
  • 출력 결과: 합계: 15

예시 4: 리스트 정렬

리스트의 요소들을 특정 기준으로 정렬하는 코드입니다.

import java.util.Arrays;
import java.util.List;

public class LambdaExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("John", "Jane", "Jack", "Doe");

        // 이름의 길이로 정렬
        List<String> sortedNames = names.stream()
            .sorted((s1, s2) -> Integer.compare(s1.length(), s2.length()))
            .collect(Collectors.toList());

        // 결과 출력
        System.out.println(sortedNames);
    }
}

코드 설명:

  • .sorted((s1, s2) -> Integer.compare(s1.length(), s2.length())): 두 문자열의 길이를 비교해 정렬합니다. 람다 표현식을 사용해 두 문자열의 길이를 비교하여 정렬 순서를 결정합니다.
  • 출력 결과: [Doe, John, Jane, Jack]

예시 5: 리스트 요소 출력

리스트의 모든 요소를 출력하는 가장 간단한 예시입니다.

import java.util.Arrays;
import java.util.List;

public class LambdaExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("John", "Jane", "Jack", "Doe");

        // 각 이름을 출력
        names.forEach(name -> System.out.println(name));
    }
}

코드 설명:

  • .forEach(name -> System.out.println(name)): 리스트의 각 요소에 대해 람다 표현식을 사용해 요소를 출력합니다. forEach는 컬렉션의 각 요소를 처리할 때 자주 사용됩니다.
  • 출력 결과:
    John
    Jane
    Jack
    Doe

요약

  • filter: 특정 조건을 만족하는 요소만 필터링할 때 사용합니다.
  • map: 각 요소를 변환할 때 사용합니다.
  • reduce: 스트림의 모든 요소를 결합하여 하나의 값으로 줄일 때 사용합니다.
  • sorted: 리스트를 정렬할 때 사용합니다.
  • forEach: 리스트의 각 요소를 처리할 때 사용합니다.

이런 람다 표현식과 스트림 API를 활용하면 코드가 훨씬 간결해지고, 명령형 코드를 줄여 함수형 프로그래밍 스타일로 작성할 수 있습니다.


.forEach()`와 `.map()

둘 다 컬렉션을 순회하는 데 사용되지만, 목적과 기능에서 중요한 차이점이 있습니다.

1. `.forEach()`
- .forEach()`는 스트림의 각 요소에 대해 특정 작업(부수 효과, side-effect)을 수행하는 메서드입니다. 주로 요소를 출력하거나 다른 메서드를 호출하는 등의 작업을 할 때 사용합니다.

- 반환값이 없습니다(`void`). 즉, 요소를 변환하거나 필터링하는 것이 아니라, 각 요소에 대해 작업을 수행하기 위한 메서드입니다.
-  주로 요소를 처리(출력, 메서드 호출 등)할 때 사용합니다.

List<String> names = Arrays.asList("John", "Jane", "Jack");
names.stream().forEach(name -> System.out.println(name));

  이 예시에서 `forEach()`는 각 이름을 출력하는 작업을 수행합니다.

2. `.map()`
- .map()은 스트림의 각 요소를 변환하여 새로운 스트림을 반환하는 메서드입니다. 입력값을 다른 값으로 변환할 때 사용합니다.
- 새로운 스트림을 반환합니다. 이 새로운 스트림은 변환된 요소들로 구성되어 있습니다.
- 주로 리스트의 요소를 다른 값(또는 타입)으로 변환할 때 사용합니다. 예를 들어, 문자열을 대문자로 변환하거나 객체를 다른 형식으로 매핑할 때 사용합니다.

List<String> names = Arrays.asList("John", "Jane", "Jack");
List<String> upperCaseNames = names.stream()
                                   .map(name -> name.toUpperCase())
                                   .collect(Collectors.toList());

  이 예시에서 `map()`은 각 이름을 대문자로 변환한 스트림을 생성하고, 그 결과를 리스트로 수집합니다.


 

람다 표현식에서 자주 사용하는 메서드들을 정리한 표를 아래에 제공합니다. 이 표에서는 스트림 API와 함께 사용하는 함수형 인터페이스의 메서드를 다룹니다. 자바 8부터 함수형 프로그래밍 스타일을 지원하기 위해 제공된 주요 메서드와 그 역할을 정리하였습니다.

메서드 이름 함수형 인터페이스 설명 예시
forEach Consumer<T> 스트림의 각 요소에 대해 주어진 작업을 수행 names.forEach(name -> System.out.println(name));
filter Predicate<T> 조건을 만족하는 요소만 필터링 stream.filter(x -> x > 10)
map Function<T, R> 스트림의 각 요소를 변환하여 새로운 스트림 생성 stream.map(x -> x * 2)
reduce BinaryOperator<T> 스트림의 요소들을 결합하여 하나의 결과값으로 줄임 stream.reduce(0, (a, b) -> a + b)
sorted Comparator<T> 스트림의 요소들을 정렬 stream.sorted((a, b) -> a.compareTo(b))
collect Collector<T, A, R> 스트림의 결과를 컬렉션으로 변환 stream.collect(Collectors.toList())
anyMatch Predicate<T> 스트림의 요소 중 하나라도 조건을 만족하는지 확인 stream.anyMatch(x -> x > 10)
allMatch Predicate<T> 스트림의 모든 요소가 조건을 만족하는지 확인 stream.allMatch(x -> x > 0)
noneMatch Predicate<T> 스트림의 모든 요소가 조건을 만족하지 않는지 확인 stream.noneMatch(x -> x < 0)
findFirst Optional<T> 스트림에서 첫 번째 요소를 반환 stream.findFirst()
findAny Optional<T> 스트림의 요소 중 하나를 반환 stream.findAny()
flatMap Function<T, Stream<R>> 각 요소를 스트림으로 변환하고, 이를 하나의 스트림으로 결합 stream.flatMap(x -> Arrays.stream(x.split(" ")))
distinct - 중복된 요소를 제거한 스트림 생성 stream.distinct()
limit - 스트림의 크기를 지정한 수만큼 제한 stream.limit(5)
skip - 처음 n개의 요소를 건너뛰고 나머지 스트림을 반환 stream.skip(2)
count - 스트림의 요소 수를 반환 stream.count()
max Comparator<T> 스트림에서 최대값을 반환 stream.max(Comparator.naturalOrder())
min Comparator<T> 스트림에서 최소값을 반환 stream.min(Comparator.naturalOrder())

설명

  1. Consumer: 입력을 받아 처리하지만, 반환값이 없는 함수형 인터페이스입니다. forEach가 이 인터페이스를 사용합니다.
  2. Predicate: 입력을 받아 true 또는 false를 반환하는 조건식입니다. filter, anyMatch, allMatch, noneMatch 등이 이 인터페이스를 사용합니다.
  3. Function<T, R>: 입력을 받아 결과로 변환하는 함수형 인터페이스입니다. map, flatMap 등이 이 인터페이스를 사용합니다.
  4. BinaryOperator: 두 개의 입력을 받아 하나의 결과를 반환하는 함수형 인터페이스입니다. reduce에서 사용됩니다.
  5. Comparator: 두 개의 객체를 비교하는 함수형 인터페이스로, 정렬에 사용됩니다.

이 표에 정리된 메서드들은 자바 람다 표현식을 활용한 스트림 API에서 가장 자주 사용되는 메서드들입니다. 각 메서드는 특정 상황에 맞게 리스트, 배열 등 컬렉션의 요소들을 처리하거나 변환할 때 사용됩니다.

728x90