본문 바로가기
Java

동작 파라미터화를 통해 코드 전달하기

by yongckim 2022. 11. 2.
728x90
반응형

동작 파라미터화 란?

동작 파라미터화는 아직 어떻게 실행될지 정해지지 않은 코드 블록을 의미합니다.

해당 코드 블록은 전달한 곳에서 필요시에 호출하기 때문에 실행이 뒤로 미루어지게 됩니다.

 

왜 실행 방법이 정해지지 않은 코드 블록이 필요하고 어떠한 장점을 가지는지 다음 예제를 통해 알아봅시다.

 

예를 들어, 사과 농장을 운영하고 싶다고 가정해봅시다.

현재, 수확한 사과 중 녹색사과만 걸러내서 확인하고 싶을 경우 다음과 같이 코드를 작성할 수 있을 것입니다.

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;
    }

 

위의 코드는 문제 없이 잘 동작하지만 녹색의 사과만 반환해줄 수 있다는 것입니다.

만약, 빨간 사과 목록을 반환받고 싶다면 해당 메서드로는 원하는 작업을 수행할 수 없게 됩니다. 

 

여기서 가장 간단한 해결 방법은 filterRedApples라는 메서드를 만들고 if 문의 조건을 빨간 사과로 바꾸는 다음과 같은 형태를 가지게 될 것 입니다.

public static List<Apple> filterRedApples(List<Apple> inventory) {
        List<Apple> result = new ArrayList<>();

        for (Apple apple : inventory) {
            if (RED.equals(apple.getColor())) {
                result.add(apple);
            }
        }
        return result;
    }

이런식으로 요구 사항에 맞게 기능을 계속해서 제공할 수 있겠지만 다른 색상의 사과(노란색, 하얀색, 검은색 등등..)에 대한 분류가 필요한 경우 계속해서 메서드를 추가해야 할 것 입니다.

그러다보면 중복되는 부분이 많이 생기고 늘어나는 코드는 가독성을 해치게 될 것입니다.

 

그래서 거의 비슷한 코드가 반복 존재하게 된다면 해당 코드를 추상화하는 것이 좋습니다.

추가 파라미터를 통해 좀 더 유연한 코드 만들기

지금 상태에서 색깔이라는 값을 받아 좀 더 유연한 코드를 만들어 봅시다.

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;
    }

 

 

이제 입력한 색깔은 유연하게 받을 수 있게 되었지만 다른 조건으로 필터링을 할 수는 없는 상태입니다.

 

예를 들어, 사과 무게를 기준으로 목록을 확인하고 싶을 경우는 현재 기능으로는 처리가 불가능합니다.

 

이를 처리하기 위해 앞서 사과 색깔 별 메서드를 만들었던 것 처럼 다음과 같이 무게를 기준으로 필터링하는 메서드를 만들 수 있을 것입니다.

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 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;
    }

 

해당 코드는 다음과 같이 사용할 수 있습니다.

Apple.filterApples(inventory, Color.GREEN, 0, true);
Apple.filterApples(inventory, null, 100, false);

 

하지만 다음 코드는 각각의 파라미터들이 어떤 의미를 가지는지 파악하기 어렵습니다.

그리고 요구사항이 바뀌었을 때 대응하기도 어렵습니다.

 

동작 파라미터화 사용하기

이런 상황에서 동작 파라미터화를 사용하면 깔끔하게 처리할 수 있습니다.

 

앞서 작성한 코드를 보면 조건식을 제외한 코드들이 계속해서 중복하고 있는 것을 볼 수 있습니다.

그렇다면 조건식을 파라미터로 보낸다면 지금 문제를 해결하고 유연한 코드를 작성할 수 있을 것입니다.

 

메서드 자체를 파라미터로 보낼 수는 없기 때문에 먼저, 다음과 같이 선택 조건을 결정하는 인터페이스를 만들어봅시다. (다양한 방식으로 구현하기 위해서)

public interface ApplePredicate {

    boolean test(Apple apple);
}

 

true 또는 false만 반환하는 함수를 프레디케이트라고 합니다.

 

이제 다음 예제처럼 ApplePredicate를 구현하여 다양한 조건식을 만들 수 있습니다.

public class AppleGreenColorPredicate implements ApplePredicate {

    @Override
    public boolean test(Apple apple) {
        return GREEN.equals(apple.getColor());
    }
}

 

public class AppleHeavyWeightPredicate implements ApplePredicate {

    @Override
    public boolean test(Apple apple) {
        return apple.getWeight() > 150;
    }
}

이런식으로 filter 메서드가 전달받는 클래스에 따라 다르게 동작하게 할 수 있습니다.

이를 전략 디자인 패턴이라고 부릅니다.

 

전략 디자인 패턴은 각 알고리즘을 캡슐화하는 알고리즘 패밀리를 정의해둔 다음 런타임에 알고리즘을 선택하는 방법입니다.

예제에서는 ApplePredicate가 알고리즘 패밀리고 AppleHeavyWeightPredicate, AppleGreenColorPredicate가 전략입니다.

 

이제 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;
    }

이렇게 동작 파라미터화를 통해 다양한 동작을 받아 내부에서 원하는 동작으로 실행시키게 할 수 있습니다.

 

public class AppleRedAndHeavyPredicate implements ApplePredicate {

    @Override
    public boolean test(Apple apple) {
        return RED.equals(apple.getColor())
            && apple.getWeight() > 150;
    }
}

// filter 사용시
List<Apple> redAndHeavyApples =
	filterApples(inventory, new AppleRedAndHeavyPredicate());

우리는 filterApples에 ApplePredicate라는 인터페이스를 파라미터로 전달한 후 test 메서드로 조건을 비교하고 있기 때문에

코드를 전달하는 것으로 볼 수 있습니다.

 

그리고 코드를 전달함으로써 Apple이라는 객체에 대한 모든 변화를 대응할 수 있는 유연한 코드를 작성하게 되었습니다.

익명 클래스 사용으로 코드 간소화 시키기

현재로도 충분히 유연한 코드이지만 인터페이스를 만들고 구현 클래스를 만든 다음 해당 클래스를 인스턴스화하는 과정이 필요하게 됩니다.

// 클래스를 선언하고 메서드를 선언하는 부분은 동작 파라미터화를 하기 위해 어쩔 수 없이 사용하게 됨
public class AppleGreenColorPredicate implements ApplePredicate {

    @Override
    public boolean test(Apple apple) {
				// 실제 사용되는 부분
        return GREEN.equals(apple.getColor());
    }
}

이를 개선하기 위해 자바의 익명클래스를 활용할 수 있습니다.

 

자바의 익명 클래스를 통해서 클래스의 선언과 인스턴스화를 동시에 수행할 수 있습니다.

다음은 익명 클래스를 활용해서 ApplePredicate를 구현하는 객체를 만드는 방법의 예시입니다.

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

이전의 코드보다는 간소화 되었지만 여전히 다음과 같은 점이 아쉽게 느껴집니다.

 

첫째, 메서드 선언과 같이 여전히 많은 추가 코드를 필요로 합니다.

둘째, 많은 프로그래머가 익명 클래스 사용에 익숙하지 않습니다.

 

위와 같은 이유로 익명 클래스로 인터페이스를 구현하는 여러 클래스를 선언하는 과정을 조금 줄일 수 있지만 여전히 만족스럽지 않습니다.

 

람다표현식으로 코드 간소화 시키기

익명 클래스 대신에 람다 표현식을 넘기면 좀 더 간결한 코드를 작성할 수 있습니다.

Apple.filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor());

코드를 보면 앞선 방식들보다 훨씬 간결하고 어떤 동작을 하는지 알아보기도 쉬워졌습니다. 

 

마지막으로 리팩토링 과정을 통해 알 수 있는 내용을 그림으로 표현하면 다음과 같습니다.

리스트의 타입을 추상화하여 더 유연하게 만들기

현재는 사과라는 객체에 대해서만 처리가 가능하지만 만약, 사과가 아닌 바나나, 포도 등을 객체로 받고 싶다면 어떻게 할까요?

 

이런 상황에서는 Generic을 통해 리스트의 타입을 추상화시켜 유연하게 만들 수 있습니다.

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
);

 

자바 8의 추가된 기능들을 활용하여 리팩토링을 해나간 결과 유연성과 간결함이라는 두마리 토끼를 잡게 되었습니다.

자바에서 동작 파라미터 사용 예시

동작 파라미터를 통해 변경에 유연한 코드를 만들 수 있었습니다.

 

이번에는 실제로 자바에서 동작 파라미터를 사용하는 함수들을 확인해봅시다.

 

Comparator로 정렬하기

자바 8의 List에는 sort라는 메서드가 포함되어 있습니다.

sort 메서드는 다음과 같이 Comparator를 이용하여 sort의 동작을 파라미터화 해줄 수 있습니다.

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

다음은 람다 표현식을 사용하여 동작 파라미터를 활용한 예시입니다.

 

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

Runnable로 코드 블록 실행하기

자바에서 병렬로 코드를 실행하기 위해서 Runnable 인터페이스를 많이 사용합니다.

자바 8까지는 Thread 생성자에 객체만을 전달할 수 있었기 때문에 결과를 반환하지 않는 void run 메서드를 포함하는 익명 클래스 Runnable을 많이 사용했습니다.

 

다음은 람다 표현식을 사용하여 동작 파라미터를 활용한 예시입니다.

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

 

Callable을 결과로 반환하기

Runnable과 같이 멀티 쓰레드 환경에서 사용하며 Runnable 과의 차이점은 특정 타입의 객체를 리턴하게 되며, Exception을 발생시킬 수 있습니다.

 

다음은 람다 표현식을 사용하여 동작 파라미터를 활용한 예시입니다.

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

GUI 이벤트 처리하기

GUI 프로그래밍을 하는 경우 마우스 클릭이나 문자열 위로 이동하는 등의 이벤트를 수행할 상황이 발생하게 됩니다.

이때 동작 파라미터를 이용할 수 있으며, 대표적으로 자바 FX에서는 setOnAction 메서드에 EventHandler를 전달함으로써 이벤트에 어떻게 반응할지 설정할 수 있습니다.

 

다음은 람다 표현식을 사용하여 동작 파라미터를 활용한 예시입니다.

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

 

정리

  • 동작 파라미터화는 메서드 내부적으로 다양한 동작을 수행할 수 있도록 코드를 메서드 인수로 전달합니다.
  • 동작 파라미터화를 통해 내부 동작을 파라미터로 전달할 수 있기 때문에 변화하는 요구사항에 대응하기 쉬워집니다.
    • 예를 들어, 값을 필터링 하는 경우 어떤 값을 기준으로 필터링을 할 것인지 상황에 따라 변경될 경우 비교하는 코드를 전달하여 상황에 맞게 원하는 값을 얻을 수 있게 됩니다.
  • 동작 파라미터는 클래스, 익명 클래스, 람다 등 다양한 방식을 이용할 수 있으며 람다를 사용하면 가장 간결한 형태로 동작을 전달할 수 있습니다.
  • 자바에서 대표적으로 Comparator, Runnable, Callable, GUI 이벤트 처리 등에서 사용되고 있습니다.

참고자료

http://www.yes24.com/Product/Goods/77125987

 

모던 자바 인 액션 - YES24

자바 1.0이 나온 이후 18년을 통틀어 가장 큰 변화가 자바 8 이후 이어지고 있다. 자바 8 이후 모던 자바를 이용하면 기존의 자바 코드 모두 그대로 쓸 수 있으며, 새로운 기능과 문법, 디자인 패턴

www.yes24.com

 

반응형