[Java] 람다 표현식 / 함수형 인터페이스

눈치없어·2025년 1월 24일

함수형 프로그래밍

프로그래밍 패러다임 중 하나로, "함수(function)" 를 일급 시민(first-class citizen)으로 간주하며, 데이터의 상태 변화보다는 함수의 조합과 계산에 집중하는 방식을 뜻함.

주요 특징

순수 함수:

  • 같은 입력이 주어지면 항상 같은 출력을 반환하며, 부작용(Side Effect)이 없음.
  • 예: 함수가 외부 변수나 상태를 변경하지 않음.
int add(int a, int b) {
    return a + b; // 외부 상태를 변경하지 않음
}

불변성:

  • 데이터는 변경되지 않고 새로운 데이터를 생성하는 방식으로 처리.
  • 기존 값을 변경하지 않으므로 멀티스레드 환경에서 안전함.

고차 함수:

  • 함수를 인자로 받거나 함수를 반환할 수 있는 함수.
  • 예: filter, map, reduce

선언형 스타일:

  • "어떻게"가 아니라 "무엇을" 할 것인지 표현.
  • 예: 컬렉션 데이터를 명령형 루프 대신 stream()으로 처리.

자바 8 이후의 변화: 람다와 스트림 도입

람다 표현식:

  • 익명 함수를 간단하게 작성할 수 있는 표현식.
// Before Java 8
Comparator<String> comparator = new Comparator<String>() {
    @Override
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
};

// After Java 8 (Lambda)
Comparator<String> comparator = (s1, s2) -> s1.length() - s2.length();

스트림 API:

  • 컬렉션 데이터 처리에 함수형 프로그래밍의 장점을 활용할 수 있는 강력한 도구.
  • 선언형 스타일로 데이터를 필터링, 매핑, 리듀싱 가능.
List<String> names = Arrays.asList("AAA", "Bb", "C");
List<String> filteredNames = names.stream()
                                  .filter(name -> name.startsWith("A"))
                                  .collect(Collectors.toList());

함수형 인터페이스:

  • 단 하나의 추상 메서드를 가지는 인터페이스.
  • 주요 인터페이스: Function, Predicate, Consumer, Supplier

실무에서 함수형 프로그래밍이 중요한 이유

  1. 가독성과 유지보수성 증가:

    • 코드를 간결하고 명확하게 작성할 수 있음.
    • 반복되는 로직을 람다 표현식으로 추상화 가능.
  2. 병렬 처리 및 성능 향상:

    • 스트림 API는 데이터를 병렬 처리할 수 있는 강력한 기능 제공.
    • 멀티코어 환경에서 효율적.
  3. 에러 감소:

    • 순수 함수와 불변성을 사용하면 동시성 문제와 버그 발생 확률이 줄어듦.
  4. 현대 프로그래밍 트렌드에 부합:

    • 클라우드, 마이크로서비스, 빅데이터 처리 등에서 함수형 프로그래밍의 패러다임은 필수.

람다 표현식 기본

람다 표현식(Lambda Expression)

  • 람다 표현식은 익명 함수(Anonymous Function) 를 간결하게 작성할 수 있는 방식으로, 자바 8에서 도입됨.
  • 람다는 함수형 인터페이스 와 함께 사용되며, 코드를 단순하고 읽기 쉽게 만들어 줌.

기본 문법

(parameters) -> expression
  • 매개변수(parameters): 함수가 받을 입력 값
  • 화살표 연산자(->): 람다의 본체를 구분
  • 본문: 함수가 수행할 로직 (단일 표현식 또는 블록 {} 가능)

람다 표현식 예제

  1. 매개변수가 없는 경우:
Runnable r = () -> System.out.println("람다 표현식이다.");
r.run();
  1. 매개변수가 하나인 경우:
Consumer<String> printer = message -> System.out.println(message);
printer.accept("자바 람다, 쉽다.");
  1. 매개변수가 여러 개인 경우:
BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
System.out.println(sum.apply(10, 20)); // 출력: 30
  1. 블록 스타일 사용:
Function<Integer, Integer> square = x -> {
    int result = x * x;
    return result;
};
System.out.println(square.apply(5)); // 출력: 25

익명 클래스 vs 람다 표현식 비교

익명 클래스:

Comparator<Integer> comparator = new Comparator<Integer>() {
    @Override
    public int compare(Integer a, Integer b) {
        return a - b;
    }
};

람다 표현식:

Comparator<Integer> comparator = (a, b) -> a - b;


특징익명 클래스람다 표현식
문법복잡하고 장황함간결하고 직관적
클래스 생성새로운 익명 클래스 생성기존 함수형 인터페이스를 구현
this 키워드익명 클래스 자신을 가리킴람다 표현식이 정의된 외부 클래스를 가리킴

람다 표현식과 함수형 인터페이스의 연계

람다 표현식은 함수형 인터페이스와 함께 사용됨.

주요 함수형 인터페이스

  1. Function<T, R>:

    • 하나의 입력을 받아 변환된 값을 반환.
    Function<Integer, String> intToString = num -> "Number: " + num;
    System.out.println(intToString.apply(5)); // 출력: Number: 5
  2. Consumer:

    • 값을 받아서 처리하지만 반환값이 없음.
    Consumer<String> printer = message -> System.out.println(message);
    printer.accept("함수형 인터페이스!");
  3. Predicate:

    • 값을 받아 조건에 따라 true/false 반환.
    Predicate<Integer> isEven = num -> num % 2 == 0;
    System.out.println(isEven.test(4)); // 출력: true
  4. Supplier:

    • 값을 생성하여 반환.
    Supplier<Double> random = () -> Math.random();
    System.out.println(random.get());

람다 표현식은 익명 클래스의 복잡함을 줄이고 함수형 프로그래밍을 쉽게 구현할 수 있게 해주는 핵심 요소.
함수형 인터페이스와의 연계를 통해 실무에서 반복 작업을 간소화하고, 더 읽기 좋은 코드를 작성할 수 있다.


람다 표현식의 한계와 주의점

람다 표현식은 코드의 간결성과 가독성을 높이는 데 매우 유용하지만, 몇 가지 한계와 주의점이 있음.

  1. 복잡한 로직에 부적합:

    • 람다는 간단한 로직에 적합하며, 복잡한 로직은 메서드로 분리하는 것이 가독성과 유지보수성에 더 좋음.
    // 복잡한 로직이 람다에 포함된 예 (비추천)
    list.stream()
        .filter(item -> {
            // 여러 조건을 확인하는 복잡한 코드
            if (item.isActive() && item.getValue() > 10) {
                return true;
            }
            return false;
        });
    
    // 복잡한 로직은 메서드로 분리 (추천)
    list.stream()
        .filter(this::isValidItem);
    
    private boolean isValidItem(Item item) {
        return item.isActive() && item.getValue() > 10;
    }
  2. 디버깅의 어려움:

    • 람다는 익명 함수로 구현되기 때문에 디버깅 시 호출 스택에서 의미 있는 정보를 확인하기 어려움.
  3. 성능 이슈:

    • 반복적으로 실행되는 람다 표현식에서 객체가 생성되거나, 컬렉션이 과도하게 생성될 경우 성능에 영향을 미칠 수 있음.
  4. 익명 클래스와의 차이점:

    • 익명 클래스는 다중 메서드를 가질 수 있지만, 람다 표현식은 단일 추상 메서드만 사용할 수 있음.
  5. 함수형 인터페이스를 정확히 이해해야 함:

    • 람다 표현식은 함수형 인터페이스와 함께 작동하므로, 인터페이스의 역할과 메서드를 정확히 이해해야 예상치 못한 동작을 방지할 수 있음.

실무 활용 사례

컬렉션과 스트림 API를 활용한 데이터 처리

예제 1: 리스트 필터링

고객 이름 리스트에서 'A'로 시작하는 이름만 필터링:

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

public class StreamExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Aaa", "Bbb", "Ccc", "AAA");

        List<String> filteredNames = names.stream()
                                          .filter(name -> name.startsWith("A"))
                                          .collect(Collectors.toList());

        System.out.println(filteredNames); // 출력: [Aaa, AAA]
    }
}

예제 2: 데이터 매핑

숫자 리스트를 문자열로 변환하고, 모든 값에 접두사 추가:

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

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

        List<String> mapped = numbers.stream()
                                     .map(num -> "Number: " + num)
                                     .collect(Collectors.toList());

        System.out.println(mapped); // 출력: [Number: 1, Number: 2, ...]
    }
}

예제 3: 데이터 합산

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

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

        int sum = numbers.stream()
                         .reduce(0, Integer::sum);

        System.out.println(sum); // 출력: 15
    }
}

람다 표현식을 활용한 코드 간소화

예제 1: Comparator 정렬

이름을 길이 순서로 정렬:

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

public class ComparatorExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("CCCCCC", "BBBB", "Bb");

        // 람다 표현식으로 Comparator 간소화
        names.sort((a, b) -> a.length() - b.length());

        System.out.println(names); // 출력: [Bb, BBBB, CCCCCC]
    }
}

예제 2: Runnable 간소화

쓰레드 실행 로직 간결화:

public class RunnableExample {
    public static void main(String[] args) {
        // 기존 방식
        Runnable oldRunnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("Runnable 방식!");
            }
        };

        // 람다 표현식
        Runnable newRunnable = () -> System.out.println("람다로 간소화된 Runnable!");

        oldRunnable.run();
        newRunnable.run();
    }
}

예제 3: 이벤트 핸들링

버튼 클릭 이벤트 처리:

import javax.swing.JButton;

public class EventHandlerExample {
    public static void main(String[] args) {
        JButton button = new JButton("클릭하세요");

        // 람다 표현식으로 이벤트 핸들러 작성
        button.addActionListener(e -> System.out.println("버튼이 클릭되었다!"));
    }
}

예제 4: 필터와 정렬 조합

조건에 따라 필터링하고 정렬:

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

public class FilterSortExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");

        List<String> result = names.stream()
                                   .filter(name -> name.length() > 3)
                                   .sorted()
                                   .collect(Collectors.toList());

        System.out.println(result); // 출력: [Alice, Charlie, David]
    }
}
profile
dock 사이즈 다르잖아

0개의 댓글