Java8 In Action

[Part1] Java8 In action - Chapter2 - 1

신나게개발썰 2022. 7. 26. 17:34
반응형

해당 내용은 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 처리등을 포함한 다양한 동작으로 파라미터화할 수 있다.
반응형