Lee's Grow up

[Java/자바] 람다의 사용, 동작을 파라미터화 본문

PROGRAMMING/JAVA

[Java/자바] 람다의 사용, 동작을 파라미터화

효기로그 2020. 5. 21. 13:56
반응형

요구사항의 변화

소비자의 요구사항은 언제든지 변할 수 있기 때문에, 변화에 조금 더 유연한 방식이 필요하게 되었다.
예를 들어, 처음에는 성이 'LEE'인 사람들만 추출해주는 되는 프로그램이 있다고 가정했을 때, 갑자기 소비자가 성이 'LEE'이고, 나이가 28살 이상인 사람들의 리스트를 추출해주세요. 라고 요구사항이 변경 될 경우 어떠한 방법으로 해결할 수 있는지 예제를 통해 알아보겠습니다.

해당 내용은 Mordern Java in Action 책의 내용을 참고하여 작성하였습니다.

1. 이름에서 성을 기준으로 필터링

첫 요구사항처럼 멤버 리스트에서 성이 LEE인 사람들을 필터링 하는 코드를 아래처럼 작성할 수 있습니다.

public static List<Member> filterByLastNameLee(List<Member> members) {
        List<Member> result = new ArrayList<>();
        for(Member member : members) {
            if(LEE.equals(member.getLastname())) { 
                result.add(member);
            } 
        }
        return result;
    }

위와 같은 코드를 작성했을 경우, 성이 고정적으로 LEE 타입인 사람만 필터링이 가능하다는 문제점이 생깁니다. 여기서 만약 KIM으로도 필터링을 하고 싶다는 요구사항이 생긴다면 우리는 성을 고정으로 LEE 타입으로 두는게 아닌, 파라미터로 받는 방법으로 해결 할 수 있습니다.

2. Lastname을 파라미터화

public static List<Member> filterByLastNameLee(List<Member> members, Lastname lastname) {
        List<Member> result = new ArrayList<>();
        for(Member member : members) {
            if(lastname.equals(member.getLastname())) { 
                result.add(member);
            } 
        }
        return result;
    }

위와 같이 작성하게 되면, 성에 대해서는 유연성을 가지는 메소드를 가지게 되었습니다. 여기에 성이 아닌 나이로 조건을 주는 요구사항이 생기면 추가로 메소드의 수정이 필요하게 됩니다.

3. 나이로 필터링 추가

우리는 멤버의 성에 대해서는 유연한 메소드를 가지고 있습니다. 하지만 또 다시 어떤 조건보다 큰 나이를 가지는 사람이라는 요구 사항이 추가될 경우 동일하게 기존의 메소드를 살짝 변경해 파라미터를 추가해줍니다.

public static List<Member> filterByLastName(List<Member> members, LastName lastname, int age) {
        List<Member> result = new ArrayList<>();
        for(Member member : members) {
            if(lastname.equals(member.getLastname()) && age < member.getAge()) { 
                result.add(member);
            } 
        }
        return result;
    }

하지만 아직 문제점이 발생합니다. 점점 파라미터가 많아지기 시작했고, 추가로 둘다 말고, 어떨때는 성으로만, 어떨 때는 성과 나이로 조건을 주고 싶어요 등의 요구사항이 생기게 됩니다.
해결 방안으로 별도의 메소드를 분리하거나, 최악이지만 플래그용 boolean 변수를 선언해서 조건을 컨트롤 하는 방식으로 할 수도 있을 것입니다. 뭐가 되었든, 중복이 발생하거나, 최악의 코드가 되는건 마찬가지입니다.

4. 인터페이스를 통해 동작 파라미터화 ( 전략 패턴 )

여러 요구사항에 대응하기 위해 동작(조건)을 추상적으로 해결하는 방법을 사용합니다. 바로 디자인패턴의 전략패턴을 활용해서 필요에 따라 선택하는 방식입니다.

public interface MemberPredicate {
     boolean test(Member member);
}

public class MemberLeeLastNamePredicate implements MemberPredicate{

    @Override
    public boolean test(Member member) {
        return LEE.equals(member.getLastname());
    }
}

public class MemberKimLastNamePredicate implements MemberPredicate {

    @Override
    public boolean test(Member member) {
        return KIM.equals(member.getLastname());
    }
}

public static List<Member> filter(List<Member> members, MemberPredicate mp) {
        List<Member> result = new ArrayList<>();
        for(Member member : members) {
            if(mp.test(member)) { 
                result.add(member);
            } 
        }
        return result;
    }

위와 같이 인터페이스와, 구현 클래스를 추가해주고, 기존의 메소드를 변경해주면, 이제 조건 동작 자체를 파라미터화 할 수 있게 되고, 아래와 같이 필요한 전략을 사용해서 원하는 조건을 선택할 수 있게 됩니다.

List<Member> leeMembers = filter(members, new MemberLeeLastNamePredicate());
List<Member> kimMembers = filter(members, new MemberKimLastNamePredicate());

이제 동작이 달라도 파라미터도 안늘어나고, 비슷한 메소드 중복이 제거 되었으니 끝일까? 아쉽지만, 마찬가지로 새로운 요구조건이 생기면 추가로 클래스를 정의를 해야하거나, 사용할 때 인스턴스화 해야하는 번거로운 작업이 남아있습니다.

5. 익명 클래스와 람다

똑똑하신 분들은 그럼 구현체를 만들지말고 사용할때 익명 클래스를 사용하면 되지 않을까? 라는 생각을 하셨을겁니다.

List<Member> result1 = filter(members, new MemberPredicate() {
            @Override
            public boolean test(Member member) {
                return PARK.equals(member.getLastname());
            }
        });

        List<Member> result2 = filter(members, new MemberPredicate() {
            @Override
            public boolean test(Member member) {
                return LEE.equals(member.getLastname());
            }
        });

이제 원하는 방식으로 동작을 구현해서 사용을 할 수가 있습니다. 그러나 코드가 상당히 길어지고 메소드의 기본 구조가 되는 코드가 @Override public boolean test(Member member) {} 불필요하게 반복 됩니다.

여기서 어차피 메소드의 반환타입, 메소드명, 파라미터등 기본 기조는 정해져 있으니까 컴파일 시점에서 추론을 할 수 있지 않을까 해서 생략을 가능하게 기능을 자바8에서 추가가 되었습니다. 바로 람다입니다.
이제 위 코드를 람다로 변환하면 아래와 같이 사용할 수 있습니다.

List<Member> result1 = filter(members, (m) -> LEE.equals(m.getLastname()));
List<Member> result2 = filter(members, (m) -> LEE.equals(m.getLastname()));

위 익명 클래스를 사용한 방식과 동일한 수행 결과를 반환합니다. 이러한 방식을 지원하기 위해 java는 8버전부터 람다, 함수형 인터페이스 제공등 다양한 변화가 생겼습니다.

나아가서 filter메소드의 List<Member> 부분도 List<T> 과 같이 제네릭을 사용하게 되면 타입에 제한 없이 사용 가능한 메소드로 만들 수 있게 됩니다.

6. 디자인패턴 리팩토링

예제에서는 전략 패턴을 예제로 사용했습니다. 전략 패턴 외 템플릿 메서드, 옵저버, 역할 사슬 체인, 팩토리 등에서도 람다로 변환해서 사용할 수 있습니다.

반응형
Comments