본문으로 바로가기

Java 8 Lambda Expression - 람다식 #1

category 개발이야기/Java 2017. 9. 9. 15:58
반응형

Java 8에 있어서 가장 두드러진 부분은 람다의 적용입니다. 
따라서 완벽하지는 않지만 그래도 쓸만한 함수형 프로그래밍을 할수가 있게 되었습니다. 


이번 포스팅에서는 람다에 대한 기본적인 설명 보다는 왜 람다가 필요한지와 어떤식으로 사용될 수 있는지에 대한 단편적인 예제를 먼저 봅니다. 

람다에 대한 기본적인 설명은 #3에서 설명할 예정이며, 어렵지 않으니, 쭈욱 따라 오시면 됩니다.


동작 파라미터화 (Behavior parameterization)


람다를 이용하면 어떤 동작을 Parameter로 만들수가 있습니다.  
"함수의 인자로 어떤 동작을 하는 함수를 받을 수 있다" 라고 이해하는게더 편할 수 도 있습니다. 
이 동작은 함수를 호출하기 전까지는 아직 정해지지 않은 상태이며, 함수를 호출할 때 전달해 준 동작을 이용해서 함수 내부가 구현됩니다. 


말로는 설명이 어려우니 간단한 예제를 봅니다. 우선 Product 클래스를 만듭니다.

이 클래스에는 아래와 같은 정보가 들어갑니다. 
1. 상품명 
2. 가격 
3. 음식인지 여부
4. 제조사 
5. 파는곳



public class Product {
	private String mName;
	private int mPrice;
	private boolean mIsFood;
	private String mMadeBy;
	private String mStore;

	public Product(String name, int price, boolean food, String madeby, String storeName) {
		mName = name;
		mPrice = price;
		mIsFood = food;
		mMadeBy = madeby;
		mStore = storeName;
	}

	public String getName() {
		return mName;
	}

	public int getPrice() {
		return mPrice;
	}

	public boolean isFood() {
		return mIsFood;
	}

	public String getMadeBy() {
		return mMadeBy;
	}

	public String getStore() {
		return mStore;
	}
}


그리고 메인 함수에는 아래와 같이 list를 만들어 넣겠습니다.



public static void main(String[] args) {
	ArrayList products = new ArrayList<>();
	products.add(new Product("새우깡", 1200, true, "농심", "이마트"));
	products.add(new Product("감자깡", 1200, true, "농심", "이마트"));
	products.add(new Product("양파링", 1000, true, "농심", "홈플러스"));
	products.add(new Product("고구마칩", 3000, true, "오리온", "홈플러스"));
	products.add(new Product("자갈치", 800, true, "오리온", "홈플러스"));
	products.add(new Product("가위", 4000, false, "문방구", "코스트코"));
	products.add(new Product("청소기", 70000, false, "LG", "코스트코"));
	products.add(new Product("양주", 30000, true, "진로", "코스트코"));
	products.add(new Product("곰젤리", 4000, true, "Bear", "코스트코"));
        ...
}


상품의 Filtering


 Owner으로 부터 요구사항이 발생합니다. 
1. "새우깡"의 정보를 얻고 싶네요.



public static void main(String[] args) {
	ArrayList products = new ArrayList<>();
	...

	ArrayList filteredByName = filterByName(products, "새우깡");
}

public static ArrayList filterByName(ArrayList products, String name) {
    ArrayList filteredProducts = new ArrayList<>();
    for (Product product : products) {
        if (product.getName().equals(name)) {
	    filteredProducts.add(product);
        }
}

이름 으로 검색을 하는 함수가 하나 생겼습니다. 

개발이 완료되자 추가적인 요구사항이 쏟아집니다. 
2. 가격이 5천원 이하인 저가상품만 보고 싶습니다.



public static ArrayList filterByPrice(ArrayList products, int price) {
	ArrayList filteredProducts = new ArrayList<>();
	for (Product product : products) {
		if (product.getPrice() <= price) {
			filteredProducts.add(product);
		}
	}
        return filteredProducts;
}
4. "코스트코"에서 파는 음식물만 보고 싶습니다. 
5. "새우깡"을 "이마트"에서 천원 이하로 살수 있나요?

요구사항 만족을 위해 개별 검색 함수 및 가능한 조건을 많이 넣을 수 있는 filter도 만듭니다.

public static ArrayList filterByStoreAndName(
                      ArrayList products, String name, String store) {
	ArrayList filteredProducts = new ArrayList<>();

	for (Product product : products) {
		if (product.getName().equals(store) && product.getStore().equals(store)) {
			filteredProducts.add(product);
		}
	}

	return filteredProducts;
}

public static ArrayList filterByStoreAndNameAndFood(
       ArrayList products, String name, String store, boolean isFood) {
	ArrayList filteredProducts = new ArrayList<>();

	for (Product product : products) {
		if (product.getName().equals(store) 
            && product.getStore().equals(store)
            && product.isFood() == isFood) {
			filteredProducts.add(product);
		}
	}

	return filteredProducts;
}

여기서는 두가지 문제가 있습니다. 
첫번째, 조건의 조합에 따라 계속해서 filter를 찍어내야 합니다. 
두번째, 만능 filter에 parameter가 늘어나면서 해당 method의 사용자(호출자)는 인자가 무슨 의미지 파악하기가 쉽지 않습니다.

ArrayList<Product> filteredByXXX = filterByStoreAndNameAndFood(products, "새우깡", "코스트코", true);

각 인자의 순서나, true가 무엇인지 호출하는 곳에서는 알기 힘듭니다.


Strategy Pattern

중복코드를 막고 코드의 유연성을 만들기 위해 strategy pattern을 사용하도록 합니다. 
strategy pattern은 
변경이 되는 부분(알고리즘이 되는 부분)을 각각의 class로 분리해 냄으로써, 
코드의 중복을 피하고, 유연성을 확보할 수 있습니다. 

filter의 코드를 보면 중복되는 부분과 항상 변경이 일어나는 부분이 보입니다.


public static ArrayList<Product> filterXXX(...) {
   ... //중복되는 부분

	if (product.getName().equals(store) && product.getStore().equals(store)) {

   ... // 중복되는 부분
}


... 으로 표기한 부분은 계속 반복되는 부분이고,  표기된 if문은 항상 변경되는 부분이므로, 이부분을 코드에서 분리해 냅니다. 

그리고 해당 부분을 구현하는 FilterPredicate란 interface를 만들고 각각의 요구사항 filter class 들이 이 부분을 구현합니다. 


interface FilterPredicate {
	public abstract boolean filter(Product product);
}

class NameFilter implements FilterPredicate {
	private String[] mContents;
	
	public NameFilter(String... args) {
		mContents = args;
	}

	@Override
	public boolean filter(Product product) {

		if (product.getName().equals(mContents[0])) {
			return true;
		}

		return false;
	}
}

class NameAndStoreFilter implements FilterPredicate {
    private String[] mContents;
	
	public NameAndStoreFilter(String... args) {
		mContents = args;
	}

	@Override
	public boolean filter(Product product) {

		if (product.getName().equals(mContents[0]) && product.getStore().equals(mContents[1]) ) {
			return true;
		}

		return false;
	}
}

호출하는 부분에서는 java의 다형성을 이용해서 넘겨줍니다.

public static void main(String[] args) {
    ArrayList<Product> products = new ArrayList<>();
		...

	ArrayList<Product> filteredByName = 
                    filter(products, new NameFilter("새우깡"));
	ArrayList<Product> filteredByPrice =
                    filter(products, new NameAndStoreFilter("새우깡", "owner"));
}

public static ArrayList<Product> filter(
            ArrayList<Product> products, FilterPredicate filterInterface) {			
	ArrayList<Product> filteredProducts = new ArrayList<>();

	for (Product product : products) {
		if(filterInterface.filter(product)) {
            filteredProducts.add(product);
        }
	}
	
    return filteredProducts;
}


strategy pattern을 사용함으로써 filter함수는 변하지 않습니다.  
또한 새로운 요구사항이 추가되면 FilterPredicate interface를 구현하는 새로운 요구사항 클래스를 추가하면 됩니다.


Lambda를 이용한 Strategy pattern의 변경

Strategy pattern에서 실제 필요한 부분은, 각 요구사항 class 내부에 있는 filter(Product product) 함수의 내용입니다. 해당 내용만을 main에서 filter()넘길 방법이 없으니, 이 내용을 갖는 class를 제작하고 생성해서  filter() 함수에 class reference를 넘기는 거지요. 

람다식을 이용하면 filter()함수의 parameter로 바로 구현 내용을 
함수 형태로 만들어 넘길 수 있습니다.

ArrayList<Product> filteredByName = filter(products, (Product product) -> product.getName().equals("새우깡"));

ArrayList<Product> filteredByComplex= filter(products, 
        (Product product) -> {
        	return product.getName().equals("새우깡") && product.getStore().equals("이마트");
		});

class의 불필요한 부분은 제거하고 필요한 로직남 넘깁니다. 
이때 사용한 -> 은 아래와 같은 형태입니다. 


"함수의 인자" -> "함수의 내부 구현코드"


상세한 사용법은 다음 포스팅에서 다룹니다. 
여기서는 "아 이런 느낌이구나"라는 감만 잡으시면 됩니다.


Predicate<T> interface의 사용

Strategy pattern을 사용하면서 FilterPredicate 라는 interface를 썼습니다. 
이 interface는 
한개의 추상 메서드를 가지며,  한개의 인자를 받아 boolean을 return합니다. 
이런 형태를 
(T) -> boolean 으로 표현하며, 이를 Lambda signature라고 합니다. 



Java8에서부터 이런 interface들을 미리 framework이 만들어서 제공해 줍니다. 
많이 사용하는 형태이니, 굳이 만들어 쓰지 말라는거죠. 

(T) -> boolean의 lambda signature를 갖는 interface는 
Predicate<T>라는 interface 입니다. 
Predicate<T>는
 public abstract boolean test(T t); 라는 abstract를 갖습니다. 

따라서 아래와 같이 filter 함수를 수정할 수 있습니다.

public static void main(String[] args) {
		...
	ArrayList<Product> filteredByName2 =
                 filterPredicate(products, 
                         (Product product) -> product.getName().equals("새우깡"));

	Predicate<Product> predicate = 
                 (Product product) -> product.getName().equals("새우깡") 
                                      && product.getStore().equals("이마트");

	ArrayList<Product> filteredByPrice2 = filterPredicate(products, predicate);
    ...
}

// 함수 인자로 Predicate<T>를 사용
public static ArrayList<Product> filterPredicate(ArrayList<Product> products, Predicate<Product> filter) {
	ArrayList<Product> filteredProducts = new ArrayList<>();

	for (Product product : products) {
	    if (filter.test(product)) {
               filteredProducts.add(product);
            }
	}

	return filteredProducts;
}

이런 함수들은 java.util.function package에 정의되어 있습니다. 
상세한 부분은 다음 포스팅에 따로 언급합니다.



반응형

'개발이야기 > Java' 카테고리의 다른 글

Java 8 Comparator  (0) 2017.09.14
Java 8 String join  (2) 2017.09.13
Java 8 Lambda Expression - 람다식 #4  (0) 2017.09.12
Java 8 Lambda Expression - 람다식 #3  (0) 2017.09.12
Java 8 Lambda Expression - 람다식 #2  (1) 2017.09.12