자바 8 - lambda(람다) 함수

들어가기

어떤 상황에서 일을 하든 소비자 요구사항은 항상 바뀔 가능성이 높습니다. 따라서 변화하는 요구사항은 소프트웨어에서는 피할 수 없는 문제입니다.
이런 상황에서 조금 더 가독성있게 편리하게 사용할 수 있는 것이 Lambda(람다)라는 자바 8에서 추가된 기능이라고 할 수 있습니다.

lambda를 이해하기 전 우리는 먼저 시시각각 변하는 사용자 요구사항에 어떻게 대응해야 할까 생각을 해보아야 합니다. 새로 추가되거나 변경되는 기능이 쉽게 구현 가능하며 장기적인 관점에서는 유지 보수가 쉬워야 합니다.

동작 파라미터화

동작 파라미터화를 이용하면 자주 바뀌는 요구사항을 효과적으로 대응할 수 있습니다. 동작 파라미터화란 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미합니다.
글로만 보려하니 잘 이해가 가지 않을 수도 있습니다. 한번 예시를 살펴보겠습니다.

사과가 담긴 리스트에서 녹색 사과만 필터링 하는 기능을 구현하는 코드를 한번 보겠습니다.

public class filterGreenApples {
    public static List filterGreenApples(List inventory) {
        List result = new ArrayList();
        for (Apple apple : inventory) {
            if ("green".equals(apple.getColor()) {
                result.add(apple);
            }
        }
        return result;
    }
}

위와 같은 코드에서 과연 초록 사과 뿐만이 아닌 빨간 사과도 필터링 하고 싶으면 어떤 식으로 코드를 수정해야 할까요?
물론 코드 자체를 복사해서 “green” 부분만 “red”로 수정해도 문제는 없습니다. 하지만 나중에 다양한 색으로 필터링 하기는 어려운 관계로 조금 더 변화에 적절하게 바꿔 보겠습니다.

public class filterApplesByColor {
    public static List filterApplesByColor(List inventory, String color) {
        List result = new ArrayList();
        for(Apple apple : inventory) {
            if (apple.getColor().equals(color)) {
                result.add(apple);
            }
        }
        return result;
    }
}

이제 모든 색의 사과를 필터링 할 수 있게 됐습니다. 하지만 더 나아가 특정 색을 가진 사과를 다시 무게로 필터링을 하려면 어떻게 수정할 수 있을까요? 여러가지 방법이 있을 수 있겠지만 가장 쉽게 생각할 수 있는 방법은 다음과 같습니다.

public class filterApplesByWeight {
    // 특정 무게보다 무거운 사과만 담기
    public static List filterApplesByWeight(List inventory, int weight) {
        List result = new ArrayList();
        for(Apple apple : inventory) {
            if (apple.getWeight() > weight) {
                result.add(apple);
            }
        }
        return result;
    }
}

지금까지 세개의 예시 코드를 보았습니다. 무엇가 느껴지시지 않으신가요? 검색 조건이 추가될 때마다 파라미터가 증가한다거나 또 색으로만 검색하고 싶은 경우는 별도의 중복된 함수를 따로 둬야 한다는 문제가 있습니다.
어떤 식으로 변경을 하면 조금 더 유연하게 변경이 가능할까요? 먼저 검색 조건을 추상화 해보겠습니다.

public interface ApplePredicate {
    boolean test (Apple apple);
}

public class filterApplesWithPredicate {
    public static List filterApplesWithPredicate(List inventory, ApplePredicate p) {
        List result = new ArrayList();
        for(Apple apple : inventory) {
            if(p.test(apple)) {
                result.add(apple);
            }
        }
        return result;
    }

    public class AppleHeavyWeightPredicate implements ApplePredicate {
        @Override
        public boolean test(Apple apple) {
            return apple.getWeight() > 150;
        }
    }
}

위와 같은 코드는 어떤가요? 전략 패턴을 사용하여 메서드가 다양한 동작을 받아서 내부적으로 수행할 수 있게되어 훨씬 유연한 코드로 변화하였습니다. ApplePredicate를 적절하게 구현하는 클래스를 만들면 다양한 요구사항에 맞게 기능을 추가할 수 있습니다.
즉, 우리가 전달한 ApplePredicate 객체에 의해 filterApplesWithPredicate 메서드의 동작을 파라미터화 한 것입니다.

자! 오래 기다리셨습니다. 이제 lambda(람다)를 이야기 할때가 된 것 같습니다. 바로 직전 본 코드에서 filter 함수를 어떤 식으로 호출할 수 있을까요?
아마 다음과 같이 호출할 수 있습니다.

public class FilteringApples {
    public static void main(String...args) {
        List inventory = Arrays.asList(new Apple(90, "green"),
                                       new Apple(200, "red"),
                                       new Apple(90, "red"));

        List heavyApples = filterApplesWithPredicate(inventory, 
                                                      new AppleHeavyWeightPredicate);
    }
}

또 실제로 AppleHeavyWeightPredicate 메서드를 활용하지 않고 익명 함수로 처리할 수도 있습니다.

public class FilteringApples {
    public static void main(String...args) {
        List inventory = Arrays.asList(new Apple(90, "green"),
                                       new Apple(200, "red"),
                                       new Apple(90, "red"));

        List heavyApples = filterApplesWithPredicate(inventory, new ApplePredicate() {
            @Override
            public boolean test(Apple apple) {
                return apple.getWeight() > 150;
            }
        });
    }
}

하지만 다음과 같은 익명 클래스에서도 다소 고치고 싶은 부분이 있습니다. 바로 중복되는 부분의 코드입니다. 익명 클래스로 ApplePredicate()를 넘겨 줄 때마다 명시적으로 public boolean test까지 표현해야해 여전히 많은 공간을 차지 합니다.
또, 익명 클래스에 익숙하지 않으면 사용하기 어렵다는 문제도 발생할 수 있습니다.

이런 코드의 장황함과 익숙하지 않음을 해결해 줄 수 있는 방법이 바로 lambda(람다)입니다. 즉, lambda(람다)는 한눈에 이해할 수 있는 코드를 구현하는 데 많은 도움을 줄 수 있습니다.
명시적으로 익명 클래스처럼 객체를 만들고 새로운 동작을 정의하는 메서드를 구현하는데는 변하지 않지만 훨씬 가독성이 뛰어난 것을 확인할 수 있습니다.

이제 lambda(람다)를 활용하여 앞에서 본 예제가 어떻게 변화했는지 확인 해보겠습니다.

public class FilteringApples {
 public static void main(String...args) {
     List inventory = Arrays.asList(new Apple(90, "green"),
                                    new Apple(200, "red"),
                                    new Apple(90, "red"));

     List heavyApples = filterApplesWithPredicate(inventory, 
        // 람다 표현식
        (Apple apple) -> apple.getWeight() > 150);
 }
}

이전 보다 훨씬 코드가 간결해지지 않았나요? 코드가 간결해지면서 가독성 또한 많이 높아졌습니다. 즉, lambda(람다)는 코드의 장황성을 배제하고 프로그래머로 하여금 훨씬 좋은 코드를 짤 수 있도록 많이 사용되고 있습니다.
다시 한번 정리하자면 lambda(람다)는 익명 클래스처럼 이름이 없는 함수면서 메서드를 간결하고 장황하지 않게 인수로 전달할 수 있는 코드 블록를 의미합니다.

마치며

lambda(람다)를 설명하기에는 다소 부족한 점이 많이 있었을 것이라 생각됩니다. 틀린 부분이 있거나 이해가 잘 가지 않는 부분이 있다면 언제든지 comment 달아주세요~
이번은 겉핥기식으로 정리를 해보았지만 이후 조금 더 심화된 내용의 lambda(람다)를 정리해볼까합니다.

Share