춤추는 개발자

[Java] Lambda Expression(람다 표현식) 본문

Developer's_til/그외 개발 공부

[Java] Lambda Expression(람다 표현식)

Heon_9u 2021. 12. 27. 23:09
728x90
반응형

 

 Stream에 이어 Java8에서 추가된 Lambda. 토비의 스프링에서 Lambda와 관련된 파트를 보면 자바에서 왜 람다를 추가했는지 알 수 있다. 바로 함수형 프로그래밍을 위해서다. 단순히 추가된 문법이 아닌 프로그래밍 자체의 패러다임에 따른 내용이 추가된 것이다. 이제 추가된 API로 문제 해결을 위한 사고를 다시 한번 생각해 볼 필요가 있다.

 

✅ Lambda

 람다의 핵심은 코드의 간결화로 줄일 수 있는 코드는 모두 지우자는 것이다. 비교를 위해 Lambda를 쓰지 않은 방식의 코드를 살펴보겠다.

 

public interface Danceable {
    void hiphop(String str);
}
class Street implements Danceable {
    @Override
    public void hiphop(String str) {
        System.out.println(str + " performance");
    }
}
Danceable danceable = new Danceable() {
    @Override
    public void hiphop(String str) {
        System.out.println(str + " performance");
    }
};

 

 Danceable 인터페이스를 구현하였다. Street라는 클래스를 만들어 Danceable 인터페이스를 객체화하거나, 재사용성이 없다면 3번째 코드처럼 익명클래스 객체를 구현하는게 기존 방식이다. 어떤 방식을 쓰든 @Override를 하며 5~6줄의 코드가 필요하다.

 여기서 익명클래스 부분을 람다 표현식으로 수정해본다면 3가지 과정을 고려할 수 있다.

 

1. 이미 대상 타입에서 Danceable이라고 명시했기 때문에 new Danceable부분이 없어도 컴파일러가 추론할 수 있다.

2. 구현해야할 메서드는 hiphop() 메서드만 있어서 메서드 명칭이 없어도 된다.

3. 컴파일러가 구현해야할 인자도 추론할 수 있다.

 

이를 토대로 람다표현식으로 고쳐보면 아래와 같다.

Danceable danceable = (str) -> System.out.println(str + " performance");

 

✅ 함수형 인터페이스(Functional Interface)

 위 코드에서 구현해야될 추상 메서드가 1개인 인터페이스를 예로 들었다. 만약 메서드가 2개일때는 어떻게 해야할까? 람다 표현식으로 구현이 가능한 인터페이스는 오직 추상 메서드가 1개뿐인 인터페이스만 가능하며, 이러한 인터페이스를 바로 함수형 인터페이스라고 부른다.

 이렇게 추상 메서드가 1개뿐인 인터페이스를 알려주는 어노테이션이 존재한다. 만약, 스케일이 큰 프로젝트의 경우, 람다식으로 표현된 인터페이스를 함수형 인터페이스로 인지하기 위해 사용한다.

@FunctionalInterface
public interface Danceable {
    void hiphop(String str);
}

 

[ 상태가 없는 객체 ]

 클래스를 구현할 때, 메서드만 존재하란 법은 없다. 만약 아래처럼 인스턴스 필드가 존재하는 경우가 있다.

Danceable danceable = new Danceable() {

    private String state;

    @Override
    public void hiphop() {
        System.out.println("current state: "+state);
    }
};

 익명 객체를 구현할 때, 인스턴스 필드를 추가할 수 있다. 하지만 람다 표현식을 사용하는 경우, 인스턴스 필드를 추가할 수 없다.

 메서드와 함수의 가장 큰 차이는 메서드는 객체에 종속되어 있다는 것이다. 함수란 Input에 의해서만 Output이 달라져야 하는데 메서드는 객체에 종속적이기 때문에 Input이 달라지지 않아도 객체의 상태에 따라 Output이 바뀐다.

 그래서 람다 표현식을 사용하는 경우, 객체는 상태를 가질 수 없다.

 

[ 행위 파라미터화 ]

보통 코딩을 할 때, 데이터를 매개변수로 전달하고, 해당 데이터로 특정 행위를 하는 메서드를 구현한다. 하지만 데이터를 전달하는 것이 아닌 행위 자체를 전달하게 되면 좀 더 유연한 코드가 될 수 있다.

class Dancer {
    private String name;
    private String team;

    Dancer(String name, String team) {
        this.name = name;
        this.team = team;
    }

    // getter
}
public List<Dancer> extractYGX(List<Dancer> dancers) {
    List<Dancer> result = new ArrayList<>();

    for(Dancer dancer: dancers) {
        if("YGX".equals(dancer.getTeam())) {
            result.add(dancer);
        }
    }

    return result;
}

public List<Dancer> extractWant(List<Dancer> dancers) {
    List<Dancer> result = new ArrayList<>();

    for(Dancer dancer: dancers) {
        if("WANT".equals(dancer.getTeam())) {
            result.add(dancer);
        }
    }

    return result;
}

 

 Dancer 클래스가 있고, 해당 객체들의 List를 특정 조건으로 추출하는 2개의 메서드가 있다. 두 메서드를 보면 if문 외에는 전부 동일한 것을 알 수 있다. 이걸 행위 파라미터를 이용해 1개의 메서드로 합치면 훨씬 보기 좋을 것이다.

 

public static List<Dancer> extractDancerList(List<Dancer> dancers, Predicate<Dancer> predicate) {
    List<Dancer> result = new ArrayList<>();

    for(Dancer dancer: dancers) {
        if(predicate.test(dancer)) {
            result.add(dancer);
        }
    }

    return result;
}
List<Dancer> dancers = Arrays.asList(new Fruit("Leejung", "YGX"), 
                                      new Fruit("Yeri", "YGX"), 
                                      new Fruit("Emma", "WANT"));

// 메서드 호출
List<Fruit> ygxList = extractDancerList(dancers, new Predicate<Dancer>() {
    @Override
    public boolean test(Dancer dancer) {
        return "YGX".equals(dancer.getTeam());
    }
});

List<Fruit> wantList = extractDancerList(dancers, new Predicate<Dancer>() {
    @Override
    public boolean test(Dancer dancer) {
        return "red".equals(dancer.getTeam());
    }
});

 

 2개의 메서드는 extractDancerList라는 메서드로 깔끔하게 합쳐졌다. 하지만 해당 메서드를 호출하는 코드는 return문을 제외하고 똑같은 코드가 다시 반복된다. 그런데 Predicate<T>라는 인터페이스를 보면 추상메서드가 test() 메서드로 한 개 뿐이다. 그렇다면 이걸 다시 람다 표현식으로 고쳐보자.

List<Dancer> ygxList = extractDancerList(dancers, dancer -> "YGX".equals(dancer.getTeam());
List<Dancer> wantList = extractDancerList(dancers, dancer -> "WANT".equals(dancer.getTeam());

 

 코드가 상당히 줄어든걸 볼 수 있다. 행위 파라미터라는 기법 자체는 기존에도 익명 클래스를 이용해 매우 활용되고 있던 기법이다. Spring에서 Template Callback Pattern이라는 디자인 패턴까지 만들 정도로 이미 유용히 사용되던 기법이다.

 

✅ Reference

https://multifrontgarden.tistory.com/124?category=471239

728x90
반응형