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은 변경이 되는 부분(알고리즘이 되는 부분)을 각각의 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 (3) | 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 |