나를 기록하다
article thumbnail
반응형

모던 자바 인 액션

동적 파라미터화를 이용하면 자주 바뀌는 요구사항에 효과적으로 대응할 수 있다. 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록으로, 이 코드 블록은 나중에 프로그램에서 호출한다. 즉, 코드 블록의 실행은 나중으로 미뤄진다.

 

[예시] 컬렉션을 처리할 때 다음의 메서드 구현

  • 리스트의 모든 요소에 대해서 '어떤 동작'을 수행할 수 있음
  • 리스트 관련 작업을 끝낸 다음에 '어떤 다른 동작'을 수행할 수 있음
  • 에러가 발생하면 '정해진 어떤 다른 동작'을 수행할 수 있음

 

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

예시로 기존의 농장 재고목록 애플리케이션에 리스트에서 녹색 사과만 필터링하는 기능을 추가한다고 가정

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

  • 사과 색을 정의하는 Color enum
enum Color { RED, GREEN }

 

  • 첫 번째 시도 결과 코드
public static List<Apple> filterGreenApples(List<Apple> inventory) {
	List<Apple> result = new ArrayList<>(); // 사과 누적 리스트
    for (Apple apple : inventory) {
    	if (GREEN.equals(apple.getColor()) { // 녹색 사과만 선택
        	result.add(apple);
        }
    }
    return result;
}

 

하지만 여기서 빨간 사과도 필터링 하고 싶다면?

기존의 코드를 복사해서 새로운 메서드를 생성하고 if문의 조건을 바꾸는 방식을 고를 수 있지만 나중에 좀 더 다양한 색으로 필터링하는 등의 변화에는 적절히 대응할 수 없다. 이럴 때 다음의 규칙이 있다.

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

 

 

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

색을 파라미터화할 수 있도록 메서드에 파라미터를 추가하면 변화하는 요구사항에 좀 더 유연하게 대응하는 코드를 만들 수 있다.

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

 

다음처럼 구현한 메서드를 호출할 수 있다.

List<Apple> greenApples = filterApplesByColor(inventory, GREEN);
List<Apple> redApples = filterApplesByColor(inventory, RED);

 

가볍고 무거운 사과를 구분지을 무게도 파라미터로 추가할 수 있다.

public static List<Apple> filterApplesByColor(List<Apple> inventory, int weight) {
	List<Apple> result = new ArrayList<>();
    for (Apple apple : inventory) {
    	if (apple.getWeight() > weight) {
        	result.add(apple);
        }
    }
    return result;
}

하지만 구현 코드를 자세히 보면 색 필터링 코드와 대부분 중복된다. 이는 소프트웨어 공학의 DRY 원칙을 어기는 것이다.

DRY 원칙
Don't Repeat Yourself - 같은 것을 반복하지 마라

 

따라서 색이나 무게 중 어떤 것을 기준으로 필터링할지 가리키는 플래그를 추가할 수 있다. (실전에는 절대 사용하지 마라!!!)

 

세 번째 시도: 가능한 모든 속성으로 필터링

모든 속성을 메서드 파라미터로 추가한 모습

public static List<Apple> filterApplesByColor(List<Apple> inventory, Color color, 
                                              int weight, boolean flag) {
    List<Apple> result = new ArrayList<>();
    for (Apple apple : inventory) {
    	if ((flag && apple.getColor().equals(color)) ||
        	(!flag && apple.getWeight() > weight)) {
        	result.add(apple);
        }
    }
    return result;
}

위의 코드는 true, false가 무엇을 의미하는지 명확히 알 수 없고 요구사항이 바뀔 때 유연하게 대응하지 못한다.

위와 같은 코드보다 어떤 기준으로 사과를 필터링할 것인지 효과적으로 전달할 수 있다면 더 좋을 것이다.


동적 파라미터화

변화하는 요구사항에 좀 더 유연하게 대응하기 위해 선택 조건을 결정하는 인터페이스를 정의하자.

(참 또는 거짓을 반환하는 함수를 프레디케이트라고 한다)

public interface ApplePredicate {
    boolean test (Apple apple);
}

 

다음과 같이 다양한 선택 조건을 대표하는 여러 버전의 ApplePredicate를 정의할 수 있다.

 

  • 무거운 사과만 선택
public class AppleHeavyWeightPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return apple.getWeight() > 150;
    }
}

 

  • 녹색 사과만 선택
public class AppleGreenColorPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return GREEN.equals(apple.getColor());
    }
}

 

아래 그림처럼 ApplePredicate는 사과 선택 전략을 캡슐화하였다.

사과 선택 전략

위 조건에 따라 filter 메서드가 다르게 동작할 것이라고 예상할 수 있다. 이를 전략 디자인 패턴(strategy design pattern)이라 한다.

전략 디자인 패턴이란?
각 알고리즘(전략이라 불리는)을 캡슐화하는 알고리즘 패밀리를 정의해둔 다음에 런타임에 알고리즘을 선택하는 기법
예제에서 ApplePredicate가 알고리즘 패밀리고 AppleHeavyWeightPrdeicate와 AppleGreenColorPredicate가 전략이다.

ApplePredicate가 다양한 동작을 수행할 수 있는 이유는 filterApples에서 ApplePredicate 객체를 받아 사과의 조건을 검사하도록 메서드를 고쳐야 한다. 이렇게 동적 파라미터화, 즉 메서드가 다양한 동작(또는 전략)을 받아서 내부적으로 다양한 동작을 수행할 수 있다.

 

filterApples 메서드가 ApplePredicate 객체를 인수로 받아서 고치도록 하면 메서드 내부에서 컬렉션을 반복하는 로직과 컬렉션의 각 요소에 적용할 동작(예시의 프레디케이트)을 분리할 수 있다.

 

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

코드/동작 전달하기

public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p) {
    List<Apple> result = new ArrayList<>();
    for (Apple apple : inventory) {
        if(p.test(apple)) { //프레디케이트 객체로 사과 검사 조건을 캡슐화함
            result.add(apple);
        }
    }
    return result;
}

 

 

첫 번째 코드에 비해 훨씬 유연하며 가독성이 좋고 사용하기 편한 코드가 만들어졌다.

예시로 농부가 150그램이 넘는 빨간 사과를 검색해달라고 하자.

ApplePredicate를 적절하게 구현하는 클래스만 만들면 된다.

 

public class AppleRedAndHeavyPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
    	return RED.equals(apple.getColor())
               && apple.getWeight() > 150;
        }
    }

List<Apple> redAndHeavyApples = filterApples(inventory, new AppleRedAndHeavyPredicate());

 

이로써 filterApples 메서드 동작을 파라미터화에 성공하였다.

필터 메서드에 전략 전달

여기서 가장 중요한 구현은 test 메서드다.

메서드는 객체만 인수로 받으므로 test 메서드를 ApplePredicate 객체로 감싸서 전달해야 한다.

다음은 람다를 이용해서 여러 개의 ApplePredicate 클래스를 정의하지 않고도

"red".equals(apple.getColor()) && apple.getWeight() > 150

과 같은 표현식을 filterApples 메서드로 전달하는 방법을 다룬다.

 

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

동적 파라미터의 강점은, 컬렉션 탐색 로직과 각 항목에 적용할 동작을 분리할 수 있다는 것이다.

 

[퀴즈] 유연한 prettyPrintApple 메서드 구현하기

문제

사과 리스트를 인수로 받아 다양한 방법으로 문자열을 생성할 수 있도록 파라미터화된 prettyPrintApple 메서드를 구현하시오.
예를 들어 prettyPrintApple 메서드가 각각의 사과 무게를 출력하도록 지시할 수 있다. 혹은 각각의 사과가 무거운지, 가벼운지 출력하도록 지시할 수 있다. prettyPrintApple 메서드는 지금까지 살펴본 필터링 예제와 비슷한 방법으로 구현할 수 있다.

 

풀이

1. Apple을 인수로 받아 정해진 형식의 문자열로 반환할 수단인 AppleFormatter 인터페이스 생성

public interface AppleFormatter {
    String accept(Apple a);
}

 

2. AppleFormatter 인터페이스를 구현해 여러 포맷 동작 생성

public class AppleFancyFormatter implements AppleFormatter {
    public String accept(Apple apple) {
        String characteristic = apple.getWeight() > 150 ? "heavy" : "light";
        return "A " + characteristic + apple.getColor() + " apple";
    }
}

public class AppleSimpleFormatter implemnets AppleFormatter {
    public String accept(Apple apple) {
        return "An apple of " + apple.getWeight() + "g";
    }
}

 

3. prettyPrintApple 메서드가 AppleFormatter 객체를 인수로 받아 내부적으로 사용하도록 지시

public static void prettyPrintApple(List<Apple> inventory, AppleFormatter formatter) {
    for(Apple apple : inventory) {
        String output = formatter.accept(apple);
        System.out.println(output);
    }
}

 

4. 다양한 동작을 prettyPrintApple 메서드로 전달 가능하다. AppleFormatter의 구현을 객체화한 다음 prettyPrintApple의 인수로 전달한다.

 

  • 구현 결과 예시
prettyPrintApple(inventory, new AppleFancyFormatter());
A light green apple
A heavy red apple
...

prettyPrintApple(inventory, new AppleSimpleFormatter());
An apple of 80g
An apple of 155g
...

복잡한 과정 간소화

자바는 클래스의 선언과 인스턴스화를 동시에 수행할 수 있도록 익명 클래스(anonymous class)라는 기법을 제공한다.

 

익명 클래스

익명 클래스는 자바의 지역 클래스(local class; 블록 내부에 선언된 클래스)와 비슷한 개념이다.

익명 클래스를 이용하면 클래스 선언과 인스턴스화를 동시에 할 수 있다. 즉, 즉석에서 필요한 구현을 만들어서 사용할 수 있다.

 

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

List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
// filterApples 메서드의 동작을 직접 파라미터화함.
    public boolean test(Apple apple) {
        return RED.equals(apple.getColor());
    }
});

 

익명 클래스에는 단점이 존재한다.

  1. 익명 클래스는 많은 공간을 차지한다. 반복되어 지저분한 코드가 생성된다.
  2. 많은 프로그래머가 익명 클래스의 사용에 익숙하지 않다.

동적 파라미터화를 람다 표현식이라는 더 간단한 코드 전달 기법을 도입해서 정리해보자.

 

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

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) {
// 형식 파라미터 T 등장
    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로 정렬하기, Runnable로 코드 블록 실행하기, Callable을 결과로 반환하기, GUI 이벤트 처리하기를 다뤄보겠다.

 

Comparator로 정렬하기

컬렉션 정렬은 반복되는 프로그래밍 작업이다.

자바 8의 List에는 sort 메서드가 포함되어 있다. 다음과 같은 인터페이스를 갖는 java.util.Comparator 객체를 이용해서 sort의 동작을 파라미터화할 수 있다.

// java.util.Comparator
public interface Comparator<T> {
    int compare(T o1, T o2);
}

 

Comparator를 구현해서 sort 메서드의 동작을 다양화할 수 있다.

예시로 익명 클래스를 이용해서 무게가 적은 순서로 목록에서 사과를 정렬할 수 있다.

inventory.sort(new Comparator<Apple>() {
    public int compare(Apple a1, Apple a2) {
        return a1.getWeight().compareTo(a2.getWeight());
    }
});

 

요구사항이 바뀌면 바뀐 요구사항에 맞는 Comparator를 만들어 sort 메서드에 전달할 수 있다.

람다 표현식을 이용하면 다음과 같이 코드를 구현할 수 있다.

inventory.sort(
    (Apple apple) -> a1.getWeight().compareTo(a2.getWeight()));

 

Runnable로 코드 블록 실행하기

자바 스레드를 이용하면 병렬로 코드 블록을 실행할 수 있다. 여러 스레드가 각자 다른 코드를 실행할 수 있는데 나중에 실행할 수 있는 코드를 구현할 방법이 필요하다. 자바 8까지는 Thread 생성자에 객체만을 전달할 수 있었으므로 보통 결과를 반환하지 않는 void run 메서드를 포함하는 익명 클래스가 Runnable를 구현하도록 하는 것이 일반적인 방법이었다.

 

자바에서는 Runnable 인터페이스를 이용해서 실행할 코드 블록을 지정할 수 있다.

아래 코드에서 볼 수 있는 것처럼 코드 블록을 실행한 결과는 void다.

// java.lang.Runnable
public interface Runnable {
    void run();
}

 

Runnable을 이용해서 다양한 동작을 스레드로 실행할 수 있다.

Thread t = new Thread(new Runnable() {
    public void run() {
        System.out.println("Hello world!");
    }
});

 

자바 8부터 지원하는 람다 표현식을 이용하면 다음처럼 스레드 코드를 구현할 수 있다.

Thread t = new Thread(() -> System.out.println("Hello world!"));

 

Callable을 결과로 반환하기

자바 5부터 지원하는 ExecutorService 추상화 개념의 ExceutorService 인터페이스는 태스크 제출과 실행 과정의 연관성을 끊어준다.

ExecutorService를 이용하면 태스크를 스레드 풀로 보내고 결과를 Futrue로 저장할 수 있다는 점이 스레드와 Runnable을 이용하는 방식과 다르다.

 

이 개념은 뒷부분의 병렬 실행을 자세히 살펴볼 때 더 살펴볼 것이다. Callable 인터페이스를 이용해 결과를 반환하는 태스크를 만든다는 사실만 알아두자. 이 방식은 Runnable의 업그레이드 버전이라 생각할 수 있다.

// java.util.concurrent.Callable
public interface Callable<V> {
    V call();
}

 

실행 서비스에 태스크를 제출해서 위 코드를 활용할 수 있다.

다음 예제는 태스크를 실행하는 스레드의 이름을 반환한다.

ExecutorService executorService = Executors.newCacheThreadPool();
Future<String> threadName = executorService.submit(new Callable<String>() {
    @Override
    public String call()throws Exception {
        return Thread currentThread().getName();
    }
});

 

람다를 이용하면 다음처럼 코드를 줄일 수 있다.

Future<String> threadName = executorService.submit(
    () -> Thread.currentThread().getName());

 

 

GUI 이벤트 처리하기

GUI 프로그래밍은 마우스 클릭 또는 문자열 위로 이동하는 등의 이벤트에 대응하는 동작을 수행하는 식으로 동작한다.

자바FX에서는 setOnAction 메서드에 EventHandler를 전달함으로써 이벤트에 어떻게 반응할지 설정할 수 있다.

 

Button button = new Button("Send");
button.setOnAction(new EventHandler<ActionEvent>() {
    public void handle(ActionEvent event) {
        label.setText("Sent!!");
    }
});

 

즉, EventHandler는 setOnAction 메서드의 동작을 파라미터화한다. 람다 표현식으로 다음처럼 구현할 수 있다.

button.setOnAction((ActionEvent event) -> label.setText("Sent!!"));

 

정리

동적 파라미터화
- 동적 파라미터화에서는 메서드 내부적으로 다양한 동작을 수행할 수 있도록 코드를 메서드 인수로 전달
- 동적 파라미터화를 이용하면 변화하는 요구사항에 더 잘 대응할 수 있는 코드를 구현할 수 있으며 나중에 엔지니어링 비용을 줄일 수 있다.
- 코드 전달 기법을 이용하면 동작을 메서드의 인수로 전달할 수 있다.
- 자바 API의 많은 메서드는 정렬, 스레드, GUI 처리 등을 포함한 다양한 동작으로 파라미터화할 수 있다.

 

반응형
profile

나를 기록하다

@prao

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

profile on loading

Loading...