해당 내용은 Java8 In Action 책을 요약 및 정리한 내용입니다.
좀 더 자세히 알고 싶으신 분들은 책을 사서 읽어보심을 추천드립니다.!
2.1 변화하는 요구사항에 대응하기
Entity
AppleEntity.java
package Part1.Chapter2.Chapter2_2_1.entity;
public class AppleEntity {
private String color;
private int weight;
public AppleEntity() {}
public AppleEntity(String color, int weight) {
this.color = color;
this.weight = weight;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
}
Main
package Part1.Chapter2.Chapter2_2_1;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import Part1.Chapter2.Chapter2_2_1.entity.AppleEntity;
/*
* 2.1 변화하는 요구사항에 대응하기.
* 농부가 재고목록 조사를 쉽게 할 수 있는 어플리케이션이 있다고 가정하자.
*/
public class Main_2_1 {
/*
* 2.1.1 첫 번재 시도 : 녹색 사과 필터링.
* 농장 재고목록 어플리케이션 녹색 사과만 필터링 하는 기능을 추가한다.
* (해당 코드는 갑님의 변심이 생기기 전에 쓰던 코드 이므로 사용하는 곳이 없다.)
*/
public static List<AppleEntity> filterGreenApples(List<AppleEntity> inventory) {
List<AppleEntity> result = new ArrayList<>();
for(AppleEntity apple : inventory) {
if("green".equals(apple.getColor()) ) {
result.add(apple);
}
}
return result;
}
/*
* 2.1.2 두 번재 시도 : 색을 파라미터화
* 그런데 우리 갑님께서의 변심으로 녹색 사과 말고 빨간 사과도 필터링하고 싶어 하셨다.
* 다양항 색의 사과 필터링을 위해 아래와 같이 메서드를 만들었다.
*/
public static List<AppleEntity> filterAppleByColor(List<AppleEntity> inventory, String color) {
List<AppleEntity> result = new ArrayList<>();
for(AppleEntity apple : inventory) {
if(apple.getColor().equals(color)) {
result.add(apple);
}
}
return result;
}
/*
* 갑님의 계속된 요구사항을 들어보니 색과 마찬가지로 무게 기준도 얼마든지 바뀔 수 있을거 같아
* 아래와 같이 다양한 무게에 대응할 수 있도록 무게 정보 파라미터도 추가한 메서드도 만들었다.
*
* 아래 메서드가 좋은 해결책이라 할 수 있지만 구현 코드를 보면 목록을 검색하고, 각 사과에
* 필터링 조건을 적용하는 부분의 코드가 색 필터링 코드와 대부분 중복된다는 사실을 확인했다.
* 이는 소프트웨어 공학의 DRY(don't repeat yourself - 같은 것을 반복하지 말 것)원칙을 어기는 것이다.
*
* 탐색 과정을 고쳐서 성능을 개선하려 한다면? 그러면 코드 한 줄이 아닌 메서드 전체 구현을 고쳐야 한다.
* 즉, 엔지니어링적으로 비싼 대가를 치러야 한다.
*/
public static List<AppleEntity> filterAppleByWeight(List<AppleEntity> inventory, int weight) {
List<AppleEntity> result = new ArrayList<>();
for(AppleEntity apple : inventory) {
if(apple.getWeight() > weight) {
result.add(apple);
}
}
return result;
}
/*
* 비싼 대가를 치루지 않기 위해 색과 무게를 filter라는 메서드로 합치는 방법을 선택 했다.
* 해당 메서드는 색이나 무게 중 어떤 것을 기준으로 필터링할지 가라키는 플래그를 추가 했다.
*
* 그러나 형편 없는 코드이다..
* 대체 true와 false가 뭘 의미 하는지 알 수 없을 뿐더러 여기서 좀 더 다양한 요구사항이 생겼을시에
* 여러 중복된 필터 메서드를 만들거나 모든 것을 처리하는 거대한 하나의 필터 메서드를 구현해야 한다.
* 지금까지 사용한 문자열, 정수, 불린 등의 값으로 filterApples 메서드를 파라미터화 했다면 문제가 잘 정의되어 있는 상황에서의
* 이 방법이 잘 동작할 수 있다.
*/
public static List<AppleEntity> filterApples(List<AppleEntity> inventory, String color, int weight, boolean flag) {
List<AppleEntity> result = new ArrayList<>();
for(AppleEntity apple : inventory) {
if((flag && apple.getColor().equals(color))
|| (!flag && apple.getWeight() > weight)) {
result.add(apple);
}
}
return result;
}
public static void main(String[] args) {
List<AppleEntity> inventory = Arrays.asList(new AppleEntity("green", 100)
, new AppleEntity("red", 120)
, new AppleEntity("green", 150));
/*
* 2.1.2의 요구사항에 맞춰 만들었기 때문에 갑님도 만족해 할것이다.(애증의 갑님..)
*/
// 색깔 필터링.
filterAppleByColor(inventory, "red").stream()
.forEach((apple) -> System.out.println("[filterAppleByColor] color : " + apple.getColor() + ", weight : " + apple.getWeight()) );
filterAppleByColor(inventory, "green").stream()
.forEach((apple) -> System.out.println("[filterAppleByColor] color : " + apple.getColor() + ", weight : " + apple.getWeight()) );
// 무게 필터링.
filterAppleByWeight(inventory, 90).stream()
.forEach((apple) -> System.out.println("[filterAppleByWeight] weight = 90, color : " + apple.getColor() + ", weight : " + apple.getWeight()) );
filterAppleByWeight(inventory, 100).stream()
.forEach((apple) -> System.out.println("[filterAppleByWeight] weight = 100, color : " + apple.getColor() + ", weight : " + apple.getWeight()) );
// 색깔/무게 필터링.
filterApples(inventory, "", 110, false).stream()
.forEach((apple) -> System.out.println("[filterApples] weight filter = 110, color : " + apple.getColor() + ", weight : " + apple.getWeight()) );
filterApples(inventory, "green", 0, true).stream()
.forEach((apple) -> System.out.println("[filterApples] color filter = " + apple.getColor() + ", color : " + apple.getColor() + ", weight : " + apple.getWeight()) );
}
}
2.2 동작 파라미터화
Entity
AppleEntity.java
package Part1.Chapter2.Chapter2_2_2.entity;
public class AppleEntity {
private String color;
private int weight;
public AppleEntity() {}
public AppleEntity(String color, int weight) {
this.color = color;
this.weight = weight;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
}
Interface
ApplePredicate.java
package Part1.Chapter2.Chapter2_2_2.filters.inter;
import Part1.Chapter2.Chapter2_2_2.entity.AppleEntity;
public interface ApplePredicate {
boolean test(AppleEntity appleEntity);
}
Implements
AppleHeavyColorPredicate.java
package Part1.Chapter2.Chapter2_2_2.filters.impl;
import Part1.Chapter2.Chapter2_2_2.entity.AppleEntity;
import Part1.Chapter2.Chapter2_2_2.filters.inter.ApplePredicate;
public class AppleHeavyColorPredicate implements ApplePredicate {
@Override
public boolean test(AppleEntity appleEntity) {
return "green".equals(appleEntity.getColor());
}
}
AppleHeavyWeightPredicate.java
package Part1.Chapter2.Chapter2_2_2.filters.impl;
import Part1.Chapter2.Chapter2_2_2.entity.AppleEntity;
import Part1.Chapter2.Chapter2_2_2.filters.inter.ApplePredicate;
public class AppleHeavyWeightPredicate implements ApplePredicate {
@Override
public boolean test(AppleEntity appleEntity) {
return appleEntity.getWeight() > 150;
}
}
Main
package Part1.Chapter2.Chapter2_2_2;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import Part1.Chapter2.Chapter2_2_2.entity.AppleEntity;
import Part1.Chapter2.Chapter2_2_2.filters.impl.AppleHeavyColorPredicate;
import Part1.Chapter2.Chapter2_2_2.filters.impl.AppleHeavyWeightPredicate;
import Part1.Chapter2.Chapter2_2_2.filters.inter.ApplePredicate;
/*
* 2.2 동작 파라미터화
* 2.1절에서 파라미터를 추가하는 방법이 아닌 변화하는 요구사항에 좀 더 유연하게 대응할 수 있게
* 전체을 보면 우리의 선택 조건은
* 사과의 어떤 속성에 기초해서 불린값을 반환(예를 들어 사과가 녹색인가? 150그램 이상인가?)하는 방법
* 을 결정 할 수 있다.
* 이와 같은 동작을 프레디케이트라고 한다.
*
* 먼저 선택 조건을 결정하는 인터페이스를 만들자.
* (Part1.Chapter2.Chapter2_2_2.filters.inter.ApplePredicate)
*
* 그리고
* - 무거운 사과만 선택
* - 녹색 사과만 선택
* 과 같은 필터 기능을 위해 해당 인터페이스의 구현체를 만든다.
* Part1.Chapter2.Chapter2_2_2.filters.impl.AppleHeavyWeightPredicate
* 무거운 사과만 선택 하는 필터.
* Part1.Chapter2.Chapter2_2_2.filters.impl.AppleHeavyColorPredicate
* 녹색 사과만 선택하는 필터.
*
* 위 조건에 따라 filter 메서드가 다르게 동작 하는데 이를 전략 디자인 패턴(strategy design pattern)이라고 부른다.
* 전략 디자인 패턴은
* 각 알고리즘을 캡슐화하는 알고리즘 패밀리를 정의해둔 다음에 런타임에 알고리즘을 선택하는 기법이다.
* 우리 예제에서는 ApplePredicate가 알고리즘 패밀리고 AppleHeavyWeightPredicate, AppleHeavyColorPredicate가 전략이다.
* */
public class Main_2_2 {
/*
* 2.2.1 네 번째 시도 : 추상적 조건으로 필터링(이 정도면 갑님한테 돈을 더 요구 할수도 있을거 같다..)
*/
public static List<AppleEntity> filterApples(List<AppleEntity> inventory, ApplePredicate p) {
List<AppleEntity> result = new ArrayList<>();
for(AppleEntity apple : inventory) {
/*
* 해당 예제에서 가장 중요한 구현은 test 메서드 이다.
* filterApples 메서드의 새로운 동작을 정의하는 것이기 때문이다.
* 메서드는 객체만 인수로 받으므로 test 메서드를 ApplePredicate 객체로 감싸서 전달해야 한다.
* test 메서드를 구현하는 객체를 이용해서 불린 표현식 등을 전달할 수 있으므로
* 이는 '코드를 전달'할 수 있는것이나 다름없다.
*/
if(p.test(apple)) {
result.add(apple);
}
}
return result;
}
public static void main(String[] args) {
List<AppleEntity> inventory = Arrays.asList(new AppleEntity("green", 100)
, new AppleEntity("red", 120)
, new AppleEntity("green", 150));
/*
* 이렇게 동작 파라미터화, 즉 메서드가 다양한 동작(또는 전략)을 받아서 내부적으로 다양한 동작을 수행할 수 있다.
* 이렇게 함으로써 filterApples 메서드 내부에서
* 컬렉션을 반복하는 로직과 컬렉션의 각 요소에 적용할 동작을(예제에서는 프레디케이트)분리
* 한다는 점에서 소프트웨어 엔지니어링적으로 큰 이득을 얻는다.
*/
filterApples(inventory, new AppleHeavyColorPredicate()).stream()
.forEach((AppleEntity apple) -> System.out.println("[AppleHeavyColorPredicate] color : " + apple.getColor() + ", weight : " + apple.getWeight()) );
filterApples(inventory, new AppleHeavyWeightPredicate()).stream()
.forEach((AppleEntity apple) -> System.out.println("[AppleHeavyWeightPredicate] color : " + apple.getColor() + ", weight : " + apple.getWeight()) );
}
}
Quiz
Interface
ApplePrintPredicate.java
package Part1.Chapter2.Chapter2_2_2.Quiz.print.inter;
import Part1.Chapter2.Chapter2_2_2.entity.AppleEntity;
public interface ApplePrintPredicate {
String print(AppleEntity apple);
}
Implements
AppleFancyPrint.java
package Part1.Chapter2.Chapter2_2_2.Quiz.print.impl;
import Part1.Chapter2.Chapter2_2_2.Quiz.print.inter.ApplePrintPredicate;
import Part1.Chapter2.Chapter2_2_2.entity.AppleEntity;
public class AppleFancyPrint implements ApplePrintPredicate {
int checkWeight = 0;
public AppleFancyPrint(int checkWeight) {
this.checkWeight = checkWeight;
}
@Override
public String print(AppleEntity apple) {
String printStr = apple.getWeight() > checkWeight ? "heavy" : "light";
return "[Check Weight = " + this.checkWeight + ", Apple Weight = " + apple.getWeight() + "] "
+ "A " + printStr + " " + apple.getColor() + " apple";
}
}
AppleSimplePrint.java
package Part1.Chapter2.Chapter2_2_2.Quiz.print.impl;
import Part1.Chapter2.Chapter2_2_2.Quiz.print.inter.ApplePrintPredicate;
import Part1.Chapter2.Chapter2_2_2.entity.AppleEntity;
public class AppleSimplePrint implements ApplePrintPredicate {
@Override
public String print(AppleEntity apple) {
return "An " + apple.getColor() + " apple of " + apple.getWeight() + "g";
}
}
Main
package Part1.Chapter2.Chapter2_2_2.Quiz;
import java.util.Arrays;
import java.util.List;
import Part1.Chapter2.Chapter2_2_2.Quiz.print.impl.AppleFancyPrint;
import Part1.Chapter2.Chapter2_2_2.Quiz.print.impl.AppleSimplePrint;
import Part1.Chapter2.Chapter2_2_2.Quiz.print.inter.ApplePrintPredicate;
import Part1.Chapter2.Chapter2_2_2.entity.AppleEntity;
/*
* 퀴즈 2-1
* 유연한 prettyPrintApple 메서드 구현하기.
*
* 사과 리스트를 인수로 받아 다양한 방법으로 문자열을 생성(커스터마이즈된 다양한 toString 메서드와 같이)
* 할 수 있도록 파라미터화된 prettyPrintApple 메서드를 구현하시오.
* 아래에 조건을 충족하시오.
* 각각의 사과 무게를 출력하도록 지시할 수 있다.
* 각각의 사과가 무거운지, 가벼운지 출력하도록 지시할 수 있다.
*/
public class Quiz2_1 {
public static void prettyPrintApple(List<AppleEntity> inventory, ApplePrintPredicate p) {
for(AppleEntity apple : inventory) {
System.out.println(p.print(apple));
}
}
public static void main(String[] args) {
List<AppleEntity> inventory = Arrays.asList(new AppleEntity("green", 90)
, new AppleEntity("red", 120)
, new AppleEntity("green", 150));
prettyPrintApple(inventory, new AppleSimplePrint());
prettyPrintApple(inventory, new AppleFancyPrint(10));
prettyPrintApple(inventory, new ApplePrintPredicate() {
@Override
public String print(AppleEntity apple) {
return "[Anonymous Class] apple Color : " + apple.getColor()
+ ", apple weight : " + apple.getWeight();
}
});
}
}
동작 파라미터화 코드 전달하기
동작 파라미터화(behavior parameterization)란 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미.
이 코드 블록은 나중에 프로그램에서 호출한다. 즉, 코드 블록의 실행은 나중으로 미뤄진다. 예로 컬렉션을 처리할 때 다음과 같은 메서드를 구현한다고 가정하자.
- 리스트의 모든 요소에 '어떤 동작'을 수행할 수 있음.
- 리스트 관련 작업을 끝낸 다음에 '어떤 다른 동작'을 수행할 수 있음.
- 에러가 발생하면 '정해진 어떤 다른 동작'을 수행할 수 있음
같이 동작 파라미터화로 위 따옴표 영역에 다양한 기능을 수행할 수 있다.
요약
- 동작 파라미터화에서는 메서드 내부적으로 다양한 동작을 수행할 수 있도록 코드를 메서드 인수로 전달한다.
- 동작 파라미터화를 이용하면 변화하는 요구사항에 더 잘 대응할 수 있는 코드를 구현할 수 있으며 나중에 엔지니어링 비용을 줄일 수 있다.
- 코드 전달 기법을 이용하면 동작을 메서드의 인수로 전달할 수 있다. 익명 클래스로도 어느 정도 코드를 깔끔하게 만들 수 있지만 자바 8에서는 인터페이스를 상속받아 여러 클래스를 구현해야 하는 수고를 없앨 수 있는 방법을 제공한다.
- 자바 API의 많은 메서드는 정렬, 스레드, GUI 처리등을 포함한 다양한 동작으로 파라미터화할 수 있다.