카테고리 없음

[Java] 람다 표현식(Lambda Expressions) - 1탄

공대키메라 2025. 4. 29. 23:18

지난 시간에는 익명 클래스에 대해 알아보았는데,

 

익명 클래스를 설명하던중 이런 말이 있었다.

 

EventHandler<AcitonEvent>
인터페이스가 오직 하나의 메소드만 포함하기 떄문에 익명 클래스 표현식 대신에 람다 표현식을 사용할 수 있다.

 

 

여기서 람다 표현식이란 것이 대해 명확히 이해하는것이 이 글의 목적이다.

 

이번에도 친절하게도 모든 정보는 공식 문서에 정의되어 있다. 

 

이를 읽어보면서 부족한 부분은 설명에 살을 붙이도록 하겠다.

 

참고로 이 모든 글의 출처는 하단에 표시한 공식 문서에 있으니 다들 공식문서 읽어보자.

 

 

해당 글은 다음 github에 코드를 올려놨으니, 궁금한 분은 보면서 읽어도 좋다. 

 

코드 보러 가기

 

(oracle에서 제공하는 코드 약간 변경한거는 안비밀)

 


1. Lambda Expressions

익명 클래스의 한가지 문제점은 만약 우리가 선언하는 익명 클래스가 오직 메소드 하나를 포함하는 인터페이스 처럼 매우 간단하다면 익명 클래스의 문법이 효율적이지 않고 불명확하다.

 

이러한 경우엔 대개 또다른 메소드에 인수를 기능(funtionality)으로 넘기려고 한다. 

 

람다 표현식(Lambda Expressions)는 메소드 인자, 데이터로서 코드를 기능으로 다룰 수 있도록 넘길 수 있게 도와준다.

 

그래서 람다 표현식이 뭔지 정리하면?

A lambda expression is a short block of code which takes in parameters and returns a value.
Lambda expressions are similar to methods, but they do not need a name and they can be implemented right in the body of a method.

=> 파라미터를 받고 값을 반환하는 짧은 코드블록이다. 람다 표현식은 메소드와 비스샇지만, 이름이 필요없고 메소드의 바로 안에 바로 구현될 수 있다. 

 

정말로 그런지는 우리 공식 문서에서 설명해주는 글을 쭈욱 읽으면서 코드가 어떻게 변화하는지 보려고 한다.

 

2. 람다 표현식의 이상적인 사용 케이스(Ideal Use Case For Lambda Expressions)

해당 섹션은 뒤에 오는 어떻게 람다 표현식이 사용 되는지 그 과정들을 천천히 설명하고 있다.

 

필자는 너무 따라하면 재미 없으니 비슷하게 직접 코드를 만들어 보려고 한다.

 

Car.java

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;

@Getter
@ToString
@AllArgsConstructor
public class Car {
    public enum Color {
        RED, YELLOW, PURPLE;
    }

    private String name;
    private int speed;
    private String number;
    private Color color;

    public static int compareBySpeed(Car a, Car b) {
        return a.getSpeed() > a.getSpeed() ? 1 : a.getSpeed() == a.getSpeed() ? 0 : -1;
    }

}

 

 

내가 약간 수정한 코드들은 다음과 같다.

더보기

CarTest

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

public class CarTest {

    interface CheckCar{
        boolean test(Car c);
    }

    // Approach 1: Create Methods that Search for Persons that Match One
    public static void printCarFasterThan(List<Car> cars, int speed) {
        for (Car car : cars) {
            if(car.getSpeed() > 10) {
                car.showCarInfo();
            }
        }
    }

    // Approach 2: Create More Generalized Search Methods

    public static void showCarsWithinSpeed(List<Car> cars, int low, int high) {
        for (Car car : cars) {
            if(car.getSpeed() > low && car.getSpeed() < high) {
                car.showCarInfo();
            }
        }
    }

    // Approach 3: Specify Search Criteria Code in a Local Class
    // Approach 4: Specify Search Criteria Code in an Anonymous Class
    // Approach 5: Specify Search Criteria Code with a Lambda Expression
    public static void showCar(List<Car> cars, CheckCar tester) {
        for (Car c : cars) {
            if (tester.test(c)) {
                c.showCarInfo();
            }
        }
    }

    // Approach 6: Use Standard Functional Interfaces with Lambda Expressions
    public static void printPersonsWithPredicate(List<Car> cars, Predicate<Car> tester) {
        for (Car c : cars) {
            if (tester.test(c)) {
                c.showCarInfo();
            }
        }
    }

    // Approach 7: Use Lambda Expressions Throughout Your Application

    public static void processPersons(
            List<Car> cars,
            Predicate<Car> tester,
            Consumer<Car> block) {
        for (Car c : cars) {
            if (tester.test(c)) {
                block.accept(c);
            }
        }
    }

    // Approach 7, second example

    public static void processPersonsWithFunction(
            List<Car> cars,
            Predicate<Car> tester,
            Function<Car, String> mapper,
            Consumer<String> block) {
        for (Car c : cars) {
            if (tester.test(c)) {
                String data = mapper.apply(c);
                block.accept(data);
            }
        }
    }

    // Approach 8: Use Generics More Extensively
    public static <X, Y> void processElements(
            Iterable<X> source,
            Predicate<X> tester,
            Function<X, Y> mapper,
            Consumer<Y> block) {
        for (X p : source) {
            if (tester.test(p)) {
                Y data = mapper.apply(p);
                block.accept(data);
            }
        }
    }

}

 

접근 1 : 하나의 특징을 만족하는 멤버를 검색하는 메소드 생성

간단한 접근으로 하나의 특징을 만족하는 멤버를 찾는 메소드를 만들었다.

 

다음 메소드는 특정 나이보다 나이가 더 많은 멤버를 출력한다.

public static void printCarFasterThan(List<Car> cars, int speed) {
    for (Car car : cars) {
        if(car.getSpeed() > 10) {
            car.showCarInfo();
        }
    }
}

 

 

이러한 접근은 우리 application을 다루기 힘들게 만든다(brittle).

 

우리 application 을 수정하고 Person class의 데이터 구조를 변경한다고 가정하자.

 

그런다면 이 변화에 대응하려고 많은 API를 수정해야만 한다.

 

 

이게 무슨 말인가?

 

다시 정리하자면, 

 

1. 데이터 구조 변경에 취약하다

=> Person 클래스의 구조가 변경되면 (예: 나이 저장 방식) 다시 수정해야함. 물론 내부 로직을 담고 있는 메서드도.

 

2. 확장성이 부족하다.

=> 새로운 필터링 조건이 생기면 계속 새 메서드 작성이 필요하다. 조건이 복잡해지면 더 많은 메소드 필요!

 

3. 코드가 중복될 수 있다.

=> 중복되는 조건의 경우 계속 동일한 코드를 작성하게 된다!

 

이 세 가지로 정리할 수 있다. 그러니까... 접근 1 코드는 정말 OMG fxxk 이라는 말이다! ㅠㅠ...

 

접근 2 : 더 일반화된 검색 메소드 생성

public static void showCarsWithinSpeed(List<Car> cars, int low, int high) {
    for (Car car : cars) {
        if(car.getSpeed() > low && car.getSpeed() < high) {
            car.showCarInfo();
        }
    }
}

 

더 일반화된 showCarsWithinSpeed 라는 메소드를 만들었다.

 

근데 만약 특정 색깔에 따라 혹은 특정 색깔과 속도 범위에 따른 조합으로 자동차 정보를 출력하고 싶다면?

 

만약 Car 클래스를 변경하려 하고 다른 속성을 추가하려고 하면?

 

더 일반화된 method를 생성해도 각각 가능한 조회 쿼리 메소드를 작성하는건 다루기 힘든 코드를 만든다.(brittle code)

 

그러니까, 여전히 데이터 구조 변경에 취약하며, 확장성이 부족하며, 코드가 중복될 수 있다. (너의 코드 오우 쉣! ㅠㅠ)

 

 

접근 3 : Local Class에서 검색 기준 코드를 구체화

public static void showCar(List<Car> cars, CheckCar tester) {
    for (Car c : cars) {
        if (tester.test(c)) {
            c.showCarInfo();
        }
    }
}

 

 

이 메소드는 파라미터 cars 에 담긴 각각의 Card을 확인한다.

 

tester.test(c)의 결과가 true 이면 car을 출력한다. (showCarInfo메소드 호출)

 

 

public static void showCar(List<Car> cars, CheckCar tester) {
    for (Car c : cars) {
        if (tester.test(c)) {
            c.showCarInfo();
        }
    }
}

 

조회 기준을 만들기 위해 CheckCar이라는 interface 를 작성한다.

 

그 후 이 interface 의 구현체인 CheckCarOverThirtySpeedWithColor를 작성했다.

public class CheckCarOverThirtySpeedWithColor implements CarTest.CheckCar {
    @Override
    public boolean test(Car c) {
        return c.getColor() == Car.Color.RED && c.getSpeed() > 30;
    }
}

 

차 색깔이 RED 이고 스피드가 30 이상이면 true를 반환한다. 

 

그러면 위에 showCar는 CheckCarOverThirtySpeedWithColor 를 받으면 차 색깔이 RED 이고 스피드가 30 이상 인 Car만 출력한다.

 

이 method 를 사용하기 위해서, 다음과 같이 했다.

 

CarTest.showCar(cars, new CheckCarOverThirtySpeedWithColor());

 

이 방식은 덜 다루기 힘든데, Car 클래스의 데이터 구조가 변경되어도 사용한 곳의 코드를 다시 변경하지 않아도 되기 때문이다.\

 

어찌 보면 역할을 분리한 것으로 볼 수도 있다.

 

아니면 우리는 익명 클래스를 사용할 수 도 있다.

 

접근 4 : 익명 클래스로 검색 기준 코드를 구체화

CarTest.showCar(cars, new CarTest.CheckCar() {
    @Override
    public boolean test(Car c) {
        return c.getSpeed() > 100;
    }
});

 

 

새로운 클래스를 별도로 선언하지 않아도 되기 때문에 이 방식으로 우리는 코드 양을 줄였다. 

 

그래도 여전히 양이 많은데(bulky) CheckCar는 하나의 메소드만 포함하기 때문이다. 

 

이러한 경우 익명 클래스보다는 람다 표현식을 사용할 수 있다.

 

접근 5 : 람다 표현식으로 조회 조건 코드 구체화

 

여기서 CheckCars는 함수형 인터페이스이다. 함수형 인터페이스는 오직 하나의 추상 메소드를 가진 인터페이스이다.

(default랑 static method 는 선언 가능)

 

오직 하나의 추상 메소드를 포함하기 떄문에, 이름을 생략할 수 있다.

 

이를 위해, 익명 클래스를 사용하는거 대신 람다 표현식으로 사용한다.

CarTest.showCar(cars, (Car c) -> c.getSpeed() > 100);
// 혹은
CarTest.showCar(cars, c -> c.getSpeed() > 100);

 


 

자! 어떤가? 

 

사실 여기까지 긴 여정이 걸렸다.

 

 

그러면 이 람다 표현식의 장점에 대해 여기서 정리하고 가려고 한다.

 

흐름을 보면서 우리는 데이터 구조 변경에 취약하며, 확장성이 부족하며, 코드가 중복되는 초기 설정에서,

일반화된 식 -> 익명 클래스 사용 -> 람다 표현식 사용으로 점점 너저분하게 큰(bulky)한 코드가 작아지고

가독성이 좋아지는것을 확인했다.

 

 

람다 표현식 하나를 알기 위해서 여태 클래스며 익명클래스며 공식문서를 자세~히 읽어보았다.

 

하지만 여기서 끝이 아니다. 앞으로 6, 7, 8 접근이 또 있으니 더 따라가보자.

 

접근 6 : 람다 표현식으로 표준 함수형 인터페이스 사용하기

자! 우리가 만든 CheckCar를 그럼 다시 봐보자.

 

interface CheckCar{
    boolean test(Car c);
}

 

엄~청 간단한 인터페이스인데 하나의 parameter를 받고 boolean 값을 반환한다.

 

너무 간단해서 사실 그닥 우리가 실제로 사용하는 application에서는 도움이 안되니 JDK에서 정의한 몇가지 표준 함수형 인터페이스를 사용하려고 한다. 

 

이미 이러한 간단한 것들에 대해서는 JDK에서 제공을 하니 그건 나중에 이야기 하려고 한다.

 

 

예를 들어, CheckCar대신에 Predicate<T>를 사용할 수 있다. 

 

그래서 다음과 같이도 사용할 수 있다.

 

// Approach 6: Use Standard Functional Interfaces with Lambda Expressions
public static void showCarWithPredicate(List<Car> cars, Predicate<Car> tester) {
    for (Car c : cars) {
        if (tester.test(c)) {
            c.showCarInfo();
        }
    }
}

 

다음과 같이 선언 후... 사용하면?

// Approach 6: Use Standard Functional Interfaces with Lambda Expressions
CarTest.showCarWithPredicate(cars, c -> c.getSpeed() > 100);

 

기존에 직접 선언한 것과 다른게 없다.

 

표준으로 제공하는 Functional Interface 들은 기본적으로 Generic 덕분에 이렇게 사용할 수 있다.

 

여기에 오로지 람다 표현식 대신에 사용할 수 있는건 아니다.

 

접근 7 :  application 을 통해 람다 표현식 사용하기

public static void printPersonsWithPredicate(List<Car> cars, Predicate<Car> tester) {
    for (Car c : cars) {
        if (tester.test(c)) {
            c.showCarInfo();
        }
    }
}

 

메소드 showCarInfo을 호출하는 대신, tester가 지정한 기준을 만족하는 Car 인스턴스에 대해 다른 작업을 수행하도록 지정할 수 있다. 이 작업은 람다 표현식으로 지정할 수 있다.

 

showCarInfo와 유사한 기능을 하는 Car객체를 인자로 받고, void를 반환하는 함수형 인터페이스를 구현할 수 있다.

 

Consumer<T> 를 추가하면 다음과 같다.

 

// Approach 7: Use Lambda Expressions Throughout Your Application
public static void processCars(
        List<Car> cars,
        Predicate<Car> tester,
        Consumer<Car> consumer) {
    for (Car c : cars) {
        if (tester.test(c)) {
            consumer.accept(c);
        }
    }
}

 

그리고 다음과 같이 사용이 가능하다.

// Approach 7: Use Lambda Expressions Throughout Your Application
System.out.println("approach 7");
CarTest.processCars(cars, c -> c.getSpeed() > 50, c -> c.showCarInfo());

 

이렇게 우리가 직접 넘긴 람다를 통해서 조건식을 검색하고, 그 뒤에 만족하는 행위를 어떻게 처리할지 지정할 수 있다.

 


 

 

 

사실 이게 너무 자주 쓰이고 그런거긴 한데

 

그동안 너무 타인이 정리한 글에 정리하다보니 

 

공식 문서는 어떻게 정리를 하고 있을까? 하는 호기심에서 inner class 부터 람다까지 읽어보았다.

 

그리고 공식문서에서 부족한 부분은 다른 사이트를 인용해서 정리했다.

 

 

일을 한지 이제 꽤 되는데 너무 기본적인거 아닌가 하지만, 이 기본에 대해 막 사용만 했지, 어떠한 흐름과 스토리를 통해 읽을 수 있어서 참 좋은 경험은 맞는 것 같다.(살짝 지겹긴 함)

 

 

공식문서 읽기 운동의 일환으로... 그래도 읽어보았다!

 

해당 글은 너무 긴 관계로 다음에는 람다 표현식(Lambda Expression)의 문법에 대해 읽어보도록 하겠다.

 

 

출처:

https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html

https://www.w3schools.com/java/java_lambda.asp

https://docs.oracle.com/javase/tutorial/displayCode.html?code=https://docs.oracle.com/javase/tutorial/java/javaOO/examples/RosterTest.java