java

[Java] 람다의 활용

inhooo00 2025. 5. 5. 01:11

📍람다를 활용하는 예시들을 알아보자

모든 타입의 리스트를 조건에 따라 선별(filter) 할 수 있도록 하는 범용 유틸리티를 만들어보자.

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

public class GenericFilter {
    public static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
        List<T> result = new ArrayList<>();
        for (T element : list) {
            if (predicate.test(element)) {
                result.add(element);
            }
        }
        return result;
    }
}

 

리스트에 있는 특정 값을 다른 값으로 매핑(변환)하는 유틸리티 코드를 만들어보자.

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;

public class GenericMapper {
    public static <T, R> List<R> map(List<T> list, Function<T, R> mapper) {
        List<R> result = new ArrayList<>();
        for (T element : list) {
            result.add(mapper.apply(element));
        }
        return result;
    }
}

 

공통적으로 디벨롭 시킨 부분.

  • 타입 제한 해소 : Integer에만 쓸 수 있던 필터를 → String, Student 등 모든 타입에 적용 가능
  • 제네릭을 사용한 중복 제거 : 하나의 filter()로 모든 조건 처리 가능
  • 코드 재사용성 극대화 : 공통 필터 로직을 어디서든 호출 가능

 

 

📍direct() VS lambda()

direct()lambda()는 서로 다른 프로그래밍 스타일을 보여준다.

아래 코드를 확인해보자.

package lambda.lambda5.mystream;

import lambda.lambda5.filter.GenericFilter;
import lambda.lambda5.map.GenericMapper;

import java.util.ArrayList;
import java.util.List;

public class Ex1_Number {

    public static void main(String[] args) {
        // 짝수만 남기고, 남은 값의 2배를 반환
        List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        List<Integer> directResult = direct(numbers);
        System.out.println("directResult = " + directResult);

        List<Integer> lambdaResult = lambda(numbers);
        System.out.println("lambdaResult = " + lambdaResult);
    }

    static List<Integer> direct(List<Integer> numbers) {
        List<Integer> result = new ArrayList<>();
        for (Integer number : numbers) {
            if (number % 2 == 0) { // 짝수 필터링
                int numberX2 = number * 2;
                result.add(numberX2); // 2배로 변환하여 추가
            }
        }
        return result;
    }

    static List<Integer> lambda(List<Integer> numbers) {
        List<Integer> filteredList = GenericFilter.filter(numbers, n -> n % 2 == 0);
        List<Integer> mappedList = GenericMapper.map(filteredList, n -> n * 2);
        return mappedList;
    }
}

direct() 메서드(명령형 프로그래밍)를 보면 이런 특징이 있다.

  • "어떻게(How)" 수행할지를 명시
  • 개발자가 로직 흐름을 직접 제어
  • 절차 중심 → for, if, 변수 등을 사용해 명확히 기술
  • 추상화 수준 낮음

👍 장점

  • 흐름 제어에 유리함 (break, continue, 상태 추적 등)
  • 디버깅이나 세밀한 동작 파악에 좋음

👎 단점

  • 코드 길어짐, 중복 증가
  • 복잡성 증가 시 유지보수 어려움

lambda() 메서드(선언적 프로그래밍)를 보면 이런 특징이 있다.

  • "무엇(What)"을 수행할지를 명시
  • 내부 구현은 숨기고 결과 중심
  • 함수 조합 (filter, map)을 통해 의도를 직접 표현
  • 추상화 수준 높음

👍 장점

  • 코드 간결, 가독성 우수
  • 반복되는 로직 최소화 → 재사용성 향상
  • 유지보수 쉬움

👎 단점

  • 흐름 제어가 필요한 복잡한 상황엔 부적합할 수 있음
  • 함수형 스타일에 익숙하지 않으면 진입장벽 있음

 

🧾 결론

  • 명령형 프로그래밍실행 절차를 직접 제어하며, 복잡한 흐름을 다룰 때 유리하다.
  • 선언적 프로그래밍결과 중심 표현에 초점을 맞추며, 코드의 의도 전달과 유지보수 측면에서 뛰어나다.
  • 자바의 람다, filter, map은 선언형 스타일을 실현하는 핵심 도구다.

 

📍스트림 만들기

스트림(Stream) 은 데이터의 흐름을 추상화한 객체로,
데이터 소스를 반복 처리하면서 선언형 방식으로 변형 및 필터링할 수 있게 해주는 기능이다.

 

직접 이 stream을 구현해보자.

package lambda.lambda5.mystream;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;

public class MyStreamV3<T> {

    private final List<T> internalList;

    private MyStreamV3(List<T> internalList) {
        this.internalList = internalList;
    }

    // static factory method
    public static <T> MyStreamV3<T> of(List<T> internalList) {
        return new MyStreamV3<>(internalList);
    }

    // filter: 중간 연산
    public MyStreamV3<T> filter(Predicate<T> predicate) {
        List<T> filtered = new ArrayList<>();
        for (T element : internalList) {
            if (predicate.test(element)) {
                filtered.add(element);
            }
        }
        return MyStreamV3.of(filtered);
    }

    // map: 중간 연산
    public <R> MyStreamV3<R> map(Function<T, R> mapper) {
        List<R> mapped = new ArrayList<>();
        for (T element : internalList) {
            mapped.add(mapper.apply(element));
        }
        return MyStreamV3.of(mapped);
    }

    // toList: 최종 연산 (외부 반복)
    public List<T> toList() {
        return internalList;
    }

    // forEach: 최종 연산 (내부 반복)
    public void forEach(Consumer<T> consumer) {
        for (T element : internalList) {
            consumer.accept(element);
        }
    }
}

 

 

위 코드를 실행시켜보자.

package lambda.lambda5.mystream;

import java.util.List;

public class MyStreamLoopMain {

    public static void main(String[] args) {
        List<Student> students = List.of(
            new Student("Apple", 100),
            new Student("Banana", 80),
            new Student("Berry", 50),
            new Student("Tomato", 40)
        );

        // 외부 반복 방식: toList() 후 for문 사용
        List<String> result = MyStreamV3.of(students)
            .filter(s -> s.getScore() >= 80)
            .map(s -> s.getName())
            .toList(); // 리스트로 수집

        // 외부 반복: 개발자가 직접 순회
        for (String name : result) {
            System.out.println("name: " + name);
        }

        // 내부 반복 방식: forEach() 사용하여 스트림 내부에서 직접 소비
        MyStreamV3.of(students)
            .filter(s -> s.getScore() >= 80)
            .map(s -> s.getName())
            .forEach(name -> System.out.println("name: " + name));
    }
}

 

☝️ 스트림이 나온 이유는 곧, 내부 반복 vs 외부 반복의 관점이다.

  • 스트림을 사용하기 전에 일반적인 반복 방식은 `for` 문, `while` 문과 같은 반복문을 직접 사용해서 데이터를 순회하는 외부 반복(External Iteration) 방식이었다.
  • 스트림에서 제공하는 `forEach()` 메서드로 데이터를 처리하는 방식은 내부 반복(Internal Iteration) 이라고 부른다. 외부 반복처럼 직접 반복 제어문을 작성하지 않고, 반복 처리를 스트림 내부에 위임하는 방식이다.
  • 스트림 내부에서 요소들을 순회하고, 우리는 처리 로직(람다)만 정의해주면 된다.
  • 반복 제어를 스트림이 대신 수행하므로, 사용자는 반복 로직을 신경 쓸 필요가 없다. 코드가 훨씬 간결해지며, 선언형 프로그래밍 스타일을 적용할 수 있다.