Chapter2 동작 파라미터화 코드 전달하기

허준기·2023년 8월 24일
0

자바

목록 보기
8/9

동작 파라미터화

아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록

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

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

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문의 조건을 빤간 사과로 바꿔주어야 한다. 이렇게 하면 필터링할 수는 있지만 나중에 농부가 좀 더 다양한 색으로 필터링하는 변화에는 적절하게 대응할 수 없다.
이런 경우에는 거의 비슷한 코드가 반복 존재한다면 그 코드를 추상화한다.

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

enum Color{ RED, GREEN } 

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

위의 코드는 첫번째 시도에서 Color 매개변수를 추가하고 if문의 조건문을 매개변수로 들어온 Color와 비교해서 리스트에 추가한다

List<Apple> greenApples = filterApplesByColor(inventory, GREEN);
List<Applr> redApples = filterApplesByColor(inventory, 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;
}

이런식으로 코드를 바꿀수 있다.
하지만 이렇게 하면 색깔과 무게 메서드의 코드가 대부분 중복되는데, 이는 소프트웨어 공학의 DRY(Don't Repeat Yourself)원칙을 어기는 것이다.
이를 어기지 않기 위해서는 두 메서드를 하나로 합쳐서 어떤 기준으로 사과를 필터링할지 구분하는 방법을 추가해주어야 한다. → 하지만 실전에서는 전대 이 방법을 사용하면 안된다. 그 이유는 뒤에 설명

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

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

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

위의 코드는

List<Apple> greenApples = filterApples(inventory, GREEN, 0, true);
List<Apple> heavyApples = filterApples(inventory, null, 150, false);

이렇게 사용할 수 있는데 가독성이 굉장히 떨어진다 게다가 앞으로 요구사항이 바뀌었을 때 유연하게 대응할 수도 없다.

2.2 동작 파라미터화

프레디케이트 : 참 또는 거짓을 반환하는 함수

선택 조건을 결정하는 인터페이스를 정의해보자

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가 알고리즘 패밀리고 AppleWeightPredicate와 AppleGreenColorPredicate가 전략이다

그런데 ApplePredicate가 다양한 동작을 수행할 수 있도록 하려면 filterApples에서 ApplePredicate 객체를 받아 사과의 조건을 검사하도록 메소드를 고쳐야 한다.
filterApples 메소드가 ApplePredicate 객체를 인수로 받도록 고치면 메소드 내부에서 컬렉션을 반복하는 로직과 컬렉션의 각 요소에 적용할 동작을 분리할 수 있다는 점에서 소프트웨어 엔지니어링적으로 큰 이득이다

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

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

이렇게 코드를 바꾸면 만약 농부가 150g이 넘는 빨간 사과를 검색해달라고 하면 코드를 이렇게 구현할 수 있다

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

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

우리가 전달한 ApplePredicate 객체에 의해 filterApples 메소드의 동작이 결정된다.
위 코드에서 가장 중요한 구현은 test 메소드인데 filterApples 메소드의 새로운 동작을 정의한다.
메소드는 객체만 인수로 받으므로 test 메소드를 ApplePredicate 객체로 감싸서 전달해야 한다. 람다를 이용해서 여러개의 클래스를 정의하지 않고도 메소드로 전달할 수도 있다.

지금까지 살펴본 것처럼 컬렉션 탐색 로직과 각 항목에 적용할 동작을 분리할 수 있다는 것이 동작 파라미터화의 강점이다. 한 메소드가 다른 동작을 수행하도록 재활용할 수 있다. 따라서 유연한API를 만들 때 동작 파라미터화가 중요한 역할을 한다.

일단 여기까지 이론상으로는 이해했는데 이를 실전에 적용시키려면 많은 고민이 필요할 것 같다.

퀴즈 2-1 유연한 prettyPrintApple 메소드 구현하기

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

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

일단 인터페이스를 구현해줘야할 것 같다

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

boolean으로 생각했는데 출력이 되려면 String 이어야 하네..

그리고 이 인터페이스를 사용해서 다양한 클래스를 만들어야 한다
일단 사과 무게를 출력하는 클래스를 만들어보자

public class AppleFancyFormatter implements AppleFormatter{
	public String accept(Apple apple) {
    	return apple.getWeight();
    }
}

다음은 어느 사과가 무겁고 가벼운지를 출력하는 클래스이다

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

이렇게 하고 prettyPrintApple 메소드가 객체를 인수로 받아 사용하도록 해보자

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

이렇게 코드를 짜면 다양한 동작을 prettyPrintApple 메소드로 전달할 수 있다. AppleFormatter의 구현을 객체화한 다음에 prettyPrintApple의 인수로 전달한다.

prettyPrintApple(inventory, new AppleFancyFormatter());
prettyPrintApple(inventory, new AppleWeightPring());

2.3 복잡한 과정 간소화

위의 방법처럼 코드를 작성하면 filterApples 메소드로 새로운 동작을 전달하려면 ApplePredicate 인터페이스를 구현하는 여러 클래스를 정의한 다음에 인스턴스화해야 하는데 이는 상당히 번거로운 작업이며 시간 낭비다

자바는 클래스의 선언과 인스턴스화를 동시에 수행할 수 있도록 익명 클래스라는 기법을 제공한다. 이를 사용하면 코드의 양을 줄일 수는 있지만 모든 것을 해결하는 것은 아니다.

2.3.1 익명 클래스

익명 클래스는 자바의 지역 클래스(블록 내부에 선언된 클래스)와 비슷한 개념이다. 말 그대로 이름이 없는 클래스다. 익명 클래스를 이용하면 클래스 선언과 인스턴스화를 동시에 할 수 있다. → 즉석에서 필요한 구현을 만들어서 사용 가능

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

다음은 익명 클래스를 이용해서 ApplePredicate를 구현하는 객체를 만드는 방법이다

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

GUI 애플리케이션에서 이벤트 핸들러 객체를 구현할 때는 익명 클래스를 종종 사용한다.

익명클래스의 부족한 점

  • 익명 클래스는 여전히 많은 공간을 차지한다
List<Apple> resApples = filterApples(inventory, new ApplePredicate() {
  public boolean test(Apple a){
  	return RED.equals(a.getColor());
  }
});
button.setOnAction(new EventHandler<ActionEvent>(){
	public void handle(ActionEvent event){
  		System.out.println("Whoooo a click!!");	
    }

위의 코드들은 반복돼서 지저분하다

  • 많은 프로그래머가 익명 클래스의 사용에 익숙하지 않음

퀴즈 2-2 익명클래스 문제

다음 코드를 실행한 결과는 무엇일까?

public class MeaningOfThis{
	public final int value = 4;
    public void doIt(){
    	int value = 6;
        Runnable r = new Runnable(){
        	public final int value = 5;
            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는 MeaningOfThis가 아니라 Runnable을 참조하므로 5가 정답이다 → this에 대해 좀 더 공부해야겠다.

코드의 장황함(verbosity)은 나쁜 특성이다. 장황한 코드는 구현하고 유지보수하는 데 시간이 오래 걸릴 뿐 아니라 읽는 즐거움을 빼앗는 요소고, 개발자로부터 외면받는다. 한눈에 이해할 수 있어야 좋은 코드다.

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

List<Apple> result = filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor()));

자바8의 람다 표현식을 이용해서 간단하게 재구현할 수 있다

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

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

람다 표현식을 사용한 예제

List<Apple> redApples = filter(inventory, (Apple apple) -> RED.equals(apple.getColor()));
List<Integer> evenNumbers = filter(numbers, (Integer i) -> i % 2 == 0);

이렇게 하면 유연성과 간결함이라는 두 마리 토끼를 모두 잡을 수 있다

실전 예제

동작 파라미터화가 변화하는 요구사항에 쉽게 적응하는 유용한 패턴임을 확인했다
동작 파라미터화 패턴은 동작을 (한 조각의 코드로) 캡슐화한 다음에 메서드로 전달해서 메서드의 동작을 파라미터화한다.
자바 API의 많은 메서드를 다양한 동작으로 파라미터화할 수 있다 또한 이들 메서드를 익명 클래스와 자주 사용하기도 한다.

2.4.1 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 a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

2.4.2 Runnable로 코드 블록 실행하기

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

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

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

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

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

2.4.3 Callable을 결과로 반환하기

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

//java.util.concurrent.Callable
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.currentThread().getName();
   }
});

위 코드는 태스크를 실행하는 스레드의 이름을 반환한다.

람다를 이용하면

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

2.5 마치며

  • 동작 파라미터화에서는 메서드 내부적으로 다양한 동작을 수행할 수 있도록 코드를 메서드 인수로 전달한다
  • 동작 파라미터화를 이용하면 변화하는 요구사항에 더 잘 대응할 수 있는 코드를 구현할 수 있으며 나중에 엔지니어링 비용을 줄일 수 있다.
  • 코드 전달 기법을 이용하면 동작을 메소드의 인수로 전달할 수 있다. 하지만 자바8 이전에는 코드를 지저분하게 구현해야 했다. 익명 클래스로도 어느 정도 코드를 깔끔하게 만들 수 있지만 자바8에서는 인터페이스를 상속받아 여러 클래스를 구현해야 하는 수고를 없앨 수 있는 방법을 제공한다.
  • 자바 API의 많은 메소드는 정렬, 스레드, GUI 처리 등을 포함한 다양한 동작으로 파라미터화할 수 있다.
profile
나는 허준기

0개의 댓글