[Modern-Java-in-Action] 2장 : 동작 파라미터화 코드 전달하기

이동엽·2022년 12월 13일
2

java

목록 보기
12/19

이 장의 주요 내용

  • 변화하는 요구사항에 대응
  • 동작 파라미터화
  • 익명 클래스
  • 람다 표현식 미리보기

개요

우리가 어떤 상황에서 일을 하든 소비자 요구사항은 항상 바뀐다.
변화하는 요구사항은 소프트웨어 엔지니어링에서 피할 수 없는 문제다.

→ 이를 동작 파라미터화를 아용하면? 자주 바뀌는 요구사항에 효과적으로 대응할 수 있다!
→ 동작 파라미터화란 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블럭을 의미한다.


  • 동작 파라미터화는 아래처럼 다양한 기능을 수행할 수 있도록 한다.
    • 리스트에 모든 요소에 대해서 ‘어떤 동작’을 수행할 수 있음
    • 리스트 관련 작업을 끝낸 다음에 ‘어떤 다른 동작’을 수행할 수 있음
    • 에러가 발생하면 ‘정해진 어떤 다른 동작’을 수행할 수 있음

  • 단, 동작 파라미터화는 기존 동작에 추가하려면 쓸데없는 코드가 늘어난다.
    • 자바 8은 이를 람다 표현식으로 해결한다.


🔥 변화하는 요구사항에 대응하기

  • 하나의 예제를 선정한 다음 코드를 점차 개선하면서 유연한 코드를 만드는 모범사례를 보자.

첫 번째 시도 : 녹색 사과 필터링

public static List<Apple> filterGreenApples(List<Apple> inventory) {
     List<Apple> result = new ArrayList<>();
     for (Apple apple : inventory) {
         if (apple.getColor() == Color.GREEN) {
             result.add(apple);
         }
     }
     return result;
}

  • 농부가 갑자기 변심하여 빨간 사과도 필터링하고 싶어졌다.
    • 일반적으론, 메소드를 복사해서 filterRedApples() 를 만들고, 조건문만 바꾼다.
    • 하지만 이 방법으론 나중에 다른 변화에 적절하게 대응할 수 없다.

🌱 거의 비슷한 코드가 반복 존재한다면 그 코드를 추상화하라.


두 번째 시도 : 색을 파라미터화

public static List<Apple> filterApplesByColor(List<Apple> inventory, **Color color**) {
    List<Apple> result = new ArrayList<>();
    for (Apple apple : inventory) {
        if (apple.getColor() == color) {
            result.add(apple);
        }
    }
    return result;
}

  • 색을 파라미터화하여, 변화에 대응하였고 이제 아래와 같이 메소드를 호출할 수 있다.
List<Apple> greenApples = filterApplesByColor(inventory, Color.GREEN);

List<Apple> redApples = filterApplesByColor(inventory, Color.RED);

  • 갑자기 농부가 ‘색 이외에도 사과를 무게로 구분할 수 있다면 좋겠네요.’ 라고 요구한다.
    • 따라서 아래와 같이 무게 정보를 파라미터화하여 추가하였다.
public static List<Apple> filterApplesByWeight(
    List<Apple> inventory, int weight
    ) {
        List<Apple> result = new ArrayList<>();
        for (Apple apple : inventory) {
            if (apple.getWeight() > weight) {
        	    result.add(apple);
        	}
        }
        return result;
    }

  • 위 코드도 좋은 해결책이라 할 수 있지만, 아래에 동작이 코드가 중복된다.
    • 목록을 검색하고, 사과에 필터링 조건을 적용하는 부분

  • 색과 무게를 filter()라는 메서드로 합치는 방법도 있다.
    • 그러려면 어떤 기준으로 사과를 필터링할 지 구분하는 또 다른 방법이 필요하다.
    • 따라서 색이나 무게 중 어떤 것을 기준으로 할 지 정하는 플래그를 추가할 수 있다.

  • 단, 실전에서는 절대 이 방법을 사용하지 말자. (이유는 뒤에서 설명)
public static List<Apple> filterApples(
    List<Apple> inventory, Color color, int weight, boolean falg
    ) {
        List<Apple> result = new ArrayList<>();
    	 	for (Apple apple: inventory) {
    		    if ((flag && apple.getColor().equals(color) ||
    			        (!flag && apple.getWeight() > weight)) {
    						result.add(apple);
    				    }
    		}
    
    		return result;
    }
  • 위 메소드는 아래와 같이 사용해야 한다. → 정말 별로다.
List<Apple> greenApples = filterApples(inventory, GREEN, 0, true);
  • 대체 true와 false는 뭘 의미하는 걸까?
  • 게다가 요구사항이 바뀌었을 때 유연하게 대응할 수도 없다.
  • 결국 여러 중복된 필터 메서드를 만들거나, 모든 것을 처리하는 거대한 하나의 필터를 구현해야 한다.


🔥 동작 파라미터화

  • 선택 조건을 결정하는 인터페이스를 정의하자.
    • 이때, 참 또는 거짓으로 결과를 반환하는 프리디케이트를 이용한다.
public interface ApplePredicate {
    boolean test(Apple apple);
}

  • 아래처럼 다양한 선택 조건을 대표하는 여러 버전의 ApplePredicate를 정의할 수 있다.
static class AppleWeightPredicate implements ApplePredicate {
    @Override
    public boolean test(Apple apple) {
        return apple.getWeight() > 150;
    }
}
    
static class AppleColorPredicate implements ApplePredicate {    
    @Override
    public boolean test(Apple apple) {
        return apple.getColor() == Color.GREEN;
    }
}

  • 위 조건에 따라 filter 메소드가 다르게 동작할 것이라고 예상할 수 있다.
    • 이를 디자인 패턴 중 전략 패턴이라고 한다.

  • 전략 패턴은 각 알고리즘을 캡슐화하는 알고리즘 패밀리를 정의해두고, 런타임에 알고리즘을 선택한다.

  • 이제 우리는 filterApples() 에서 ApplePredicate 객체를 받아 검사하도록 수정해야 한다.
    • 즉, 메소드가 다양한 동작(또는 전략)을 받아서 내부적으로 다양한 동작을 수행할 수 있게 한다.
    • 동작 파라미터화


네 번째 시도 : 추상적 조건으로 필터링

public static List<Apple> filter(List<Apple> inventory, ApplePredicate p) {
    List<Apple> result = new ArrayList<>();
    for (Apple apple : inventory) {
        if (p.test(apple)) {
            result.add(apple);
        }
    }
    return result;
}

코드/동작 전달하기

  • 첫 번째 코드에 비해 더 유연한 코드를 얻었고, 동시에 가독성도 좋아졌다.
  • 전달한 ApplePredicate 객체에 의해 filterApples() 의 동작이 결정된다.

  • 위 그림에서 보여주듯, 가장 중요한 구현은 새로운 동작을 정의하는 test() 메서드이다.
  • 안타깝게도 메서드는 객체만 인수로 받으므로, test() 메서드를 ApplePredicate로 감싸서 전달했다.

→ 그래도 이를 구현한 객체를 이용해서 불리언 표현식을 전달하므로 ‘코드를 전달’ 한 것이나 다름없다.


한 개의 파라미터, 다양한 동작

  • 동작 파라미터화의 강점
    • 컬렉션 탐색 로직과 각 항목에 적용할 동작을 분리할 수 있다.
    • 따라서, 아래 그림처럼 한 메서드가 다른 동작을 수행하도록 재활용할 수 있다.


**응용! 유연한 prettyPrintApple 메서드 구현하기**
  • 사과 리스트를 인수로 받아 다양한 방법으로 문자열을 생성할 수 있도록 파라미터화되게 구현해보자.
    • ex) 사과가 가벼운지, 무거운지 출력하도록 지시할 수 있다.
public interface AppleFormatter {
    String accept(Apple a);
}
    
public class AppleFancyFormatter implements AppleFormatter {
	public String accept(Apple apple) {
    	String characteristic = apple.getWeight() > 150 ? "heavy" : "light";
        return "A " + characteristic + " " + apple.getColor() + " apple";
    	// ex) A light green apple
    	// ex) A heavy red apple
    }
}
    
public class AppleSimpleFormatter implements AppleFormatter {
    public String accept(Apple apple) {
    	return "An apple of " + apple.getWeight() + "g";
        // ex) An apple of 80g
    	// ex) An apple of 155g
    }
}

→ 지금까지 동작을 추상화해서 변화하는 요구사항에 대응할 수 있는 코드를 구현하는 방법을 살펴봤다.
→ 하지만, 여러 클래스를 구현해서 인스턴스화하는 과정이 거추장스러울 수 있다.
→ 이를 개선해보자!



🔥 복잡한 과정 간소화

  • 현재 filterApples() 메소드로 새로운 동작을 전달하려면?
    • ApplePredicate 인터페이스를 구현하는 여러 클래스를 정의해야 하고, 이를 인스턴스화 해야 한다.
    • 이는 상당히 번거로운 작업이며, 시간 낭비이기도 하다.

  • 자바는 클래스의 선언과 인스턴스화를 동시에 수행할 수 있도록 익명 클래스라는 기법을 제공한다.
    • 이를 이용해 코드의 양을 줄일 수 있지만, 모든 것을 해결하진 않는다.
    • 뒤에서 간단하게 람다 표현식을 이용해 더 가독성 있는 코드를 구현하는 방법을 설명한다.

익명 클래스

  • 익명 클래스는 자바의 지역 클래스와 비슷한 개념이다.
    • 말 그대로 이름이 없는 클래스로, 클래스 선언과 인스턴스화를 동시에 할 수 있다.
    • 즉, 즉석에서 필요한 구현을 만들어서 사용할 수 있다.

다섯 번째 시도 : 익명 클래스 사용

  • 익명 객체를 이용해 ApplePredicate 를 구현하는 객체를 만들기
    • filterApples 메서드의 동작을 직접 파라미터화했다.
List<Apple> redApples = filterApples(inventory, **new ApplePredicate()** {
	public boolean test(Apple apple) {
    	return RED.equals(apple.getColor());						
    }
});

  • 스윙 라이브러리를 사용했던 기억을 떠올리면, 아래와 같은 코드를 자주 접할 수 있다.
button.setOnAction(new EventHandler<ActionEvent>() {
	public void handle(ActionEvent event) {
		System.out.println("Whoooo a click!!");
    }
});

  • 하지만 익명 클래스로도 아직 부족한 점이 있다.
    • 첫째, 익명 클래스는 여전히 많은 공간을 차지한다.
List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
	public boolean test(Apple apple) {              //반복되어 지저분한 코드
    	return RED.equals(apple.getColor());						
    }
}
        
button.setOnAction(new EventHandler<ActionEvent>() {
	public void handle(ActionEvent event) {
		System.out.println("Whoooo a click!!");
	}
});

  • 둘째, 많은 프로그래머가 익명 클래스의 사용에 익숙하지 않다.
    • 아래는 많은 프로그래머를 곤경에 빠뜨린 고전 자바 문제다. 풀어보자.
/* 다음 코드를 실행한 결과는 4, 5, 6, 10, 42 중 어느 것일까? */
            
public class MeaningOfThis {
    public final int value = 4;
            
    public void doIt() {
        int value = 6;
        Runnable r = new Runnable() {
            public final int value = 5;
            @Override
            public void run() {
                int value = 10;
                System.out.println(this.value);
            }
        };
        r.run();
    }
    
    public static void main(String... args) {
         MeaningOfThis m = new MeaningOfThis();
        m.doIt(); // ???
    }
}
  • 코드에서 this는 Runnable을 참조하므로 5가 정답이다.

  • 코드의 장황함은 나쁜 특성이다.
    • 구현하고 유지보수하는 데 오랜 시간이 걸리고, 읽는 즐거움을 뺏는다.
    • 한 눈에 이해할 수 있어야 좋은 코드다.

결과

  • 익명 클래스로 인터페이스를 구현하는 여러 클래스를 선언하는 과정을 조금 줄일 수 있지만 여전히 불만족스럽다.
  • 또한 코드 조각을 전달하는 과정에서 결국은 객체를 만들고 명시적으로 새로운 동작을 정의하는 메소드를 구현해야 하는 점은 변하지 않았다.


여섯 번째 시도 : 람다 표현식

  • 자바 8의 람다 표현식을 이용해서 위 예제 코드를 아래처럼 간단하게 재구현할 수 있다.
List<Apple> result = filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor()));

  • 이전 코드보다 훨씬 간단해지고, 문제를 잘 설명하는 코드가 되었다! → 복잡성 문제 해결


일곱 번째 시도 : 리스트 형식으로 추상화

  • 아래와 같이 작성하면, 바나나, 오렌지 등의 리스트에도 필터 메소드를 사용할 수 있다.
public interface Predicate<T> {
    boolean test(T t);
}
    
public static <T> List<T> filter(List<T> list, Predicate<T> p) {
    List<T> result = new ArrayList<>();
    for(T e: list) {
        if(p.test(e)) {
            result.add(e);
        }
    }
    return result;
}

  • 람다식을 사용한 예제
List<Apple> redApples = filter(inventory, (Apple apple) -> RED.equals(apple.getColor()));
    
// 정수, 문자열의 리스트에도 사용할 수 있다.
List<Integer> evenNumbers = filter(numbers, (Integer i) -> i % 2 == 0);


🔥 실전 예제

  • 동작 파라미터화 패턴은 동작을 캡슐화한 다음에 메소드로 전달해서 메서드의 동작을 파라미터화한다.
    • 자바 API의 많은 메서드를 다양한 동작으로 파라미터화할 수 있다.
    • 또한 이들의 메서드를 익명 클래스와 자주 사용하기도 한다.


Comparator로 정렬하기

  • 컬렉션 정렬은 반복되는 프로그래밍 작업이다.
  • 자바 8의 List에는 sort() 메소드가 포함되어 있다.
    • 물론 Collections.sort도 존재한다.
public interface Comparator<T> {
	int compare(T o1, T o2);
}

  • 무게가 적은 순서로 사과 목록을 정렬하기
inventory.sort(new Comparator<Apple>() {
	public int compare(Apple a1, Apple a2) {
    	return a1.getWeight().compareTo(a2.getWeight());
    }
});

  • 람다식을 이용해 간단하게 구현하기
inventory.sort((Apple a1, Apple a2) -> a2.getWeight().compareTo(a2.getWeight()));

Runnable로 코드 블록 실행하기

  • 자바 스레드를 이용하면 병렬로 코드 블록을 실행할 수 있다.
    • 이때, 어떤 코드를 실행할 것인지를 스레드에게 알려줄 수 있을까?
    • 여러 스레드는 각자 다른 코드를 실행할 수 있다.

  • Java 8까지는 Thread 생성자에 객체만을 전달할 수 있었으므로 보통 결과를 반환하지 않는 void run() 메소드를 포함하는 익명 클래스가 Runnable 인터페이스를 구현하도록 하는 것이 일반적이다.

  • Runnable을 이용해 다양한 동작을 스레드로 실행하기
Thread t = new Thread(new Runnable() {
	public void run() {
		System.out.println("Hello world");
    }
});

  • 람다식을 이용하기
Thread t = new Thread(() -> System.out.println("Hello world"));

GUI 이벤트 처리하기

  • 자바 5부터는 ExecutorService 추상화 개념을 지원한다.
    • 이 인터페이스는 태스크 제출과 실행 과정의 연관성을 끊어준다.
    • 태스크를 스레드풀로 보내고, 결과를 Future로 저장할 수 있다는 점이 스레드와 Runnable 이용 방식과 다르다.

  • 당장은 Callable 인터페이스를 이용해 결과를 반환하는 태스크를 만든다는 사실만 알아두자.
    • 이 방식은 Runnable의 업그레이드 버전이라고 생각할 수 있다.
public interface Callable<V> {
	V call();
}

  • 태스크를 실행하는 스레드의 이름을 반환하기
ExecutorService executorService = Executors.newCachedThreadPool();
    
Future<String> threadName = executorService.submit(new Callable<String>() {
    @Override
    public String call() throws Exception {
    	return Thread.currrentThread().getName();
    }
});

  • 람다식을 이용하기
Future<String> threadName = executorService.submit(() -> Thread.currentThread().getName));

마무리

  • 동작 파라미터화 : 메서드 내부적으로 다양한 동작을 수행할 수 있도록 코드를 메서드 인수로 전달한다.
    • 이를 이용하면 변화하는 요구사항에 더 잘 대응할 수 있는 코드를 구현할 수 있고,
    • 나중에 엔지니어링 비용도 줄일 수 있다.
profile
백엔드 개발자로 등 따숩고 배 부르게 되는 그 날까지

1개의 댓글

comment-user-thumbnail
2022년 12월 16일

좋은 정보네요.

답글 달기