CHAP02 - Modern-Java-in-Action/Online-Study GitHub Wiki

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

동작을 파라미터화하면 함수를 다른 메서드의 매개인자로 넘길 수 있다. 좋은 예는 Collections.sort()메서드에 Comparator 익명클래스로 감싼 compare() 를 전달하는 경우이다.

2장에서는 이러한 동작 파라미터화가 왜 필요한지를 설명한다. 자바8 이후로 동작 파라미터화를 어떻게 하는지, 이를테면 람다표현식과 메서드 직접참조는 3장에서 구체적으로 다룬다.

메서드 : 클래스에 종속된 동작
함수 : 클래스에 종속되지 않은 동작

1. 동작 파라미터화가 왜 필요할까

컬렉션에 다음과 같은 메서드를 구현한다고 가정해보자. 이 메서드가 어떤 동작을 수행할지 정해지지 않은 상태이다. 상황에 따라 다양한 동작을 수행하고, 시시때때로 변화하는 요구사항을 맞추려면 복잡한 코드를 작성해야 하겠다.

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

그 대신에 어떤 동작을 수행할지 메소드의 매개인자로 넘겨받으면 어떨까?

2. 동작을 파라미터화하는 방법들

기본 예제코드는 다음과 같다.

enum Color {RED, GREEN}

public class Apple {
    Color color;
    Integer weight;

    Apple(Color color, int weight, int no){
        this.color = color;
        this.weight = Integer.valueOf(weight);
        this.no = Integer.valueOf(no);
    }

    public Color getColor(){return this.color;}
    public Integer getWeight(){return this.weight;}
}
List<Apple> inventory = new ArrayList<>();
inventory.add(new Apple(Color.GREEN, 500));
inventory.add(new Apple(Color.RED, 750));
inventory.add(new Apple(Color.RED, 400));
inventory.add(new Apple(Color.GREEN, 600));
inventory.add(new Apple(Color.RED, 100));
inventory.add(new Apple(Color.GREEN, 2000));

2.1. 프리디케이트

무게 또는 색을 구분하기 위해 if-else문을 각각 작성하는 대신에 해당 동작을 프리디케이트 함수에 담아 전달받을 수 있다.

프리디케이트는 참 또는 거짓을 반환하는 함수이다. 단 하나의 함수만 가지는 프리디케이트 인터페이스를 정의하고, 해당 프리디케이트를 상황에 맞게 구현하여 사용한다.

인터페이스 정의

아래와 같이 직접 정의해서 사용할 수 있다.

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

또는 java.util.function 아래 Predicate<T> 인터페이스를 구현해 사용해도 된다. boolean으로 참/거짓을 반환하는 test() 함수, 프리디케이트간 AND/OR/NOT 연산을 수행하는 함수 등이 정의되어 있다.

인터페이스 구현

프리디케이트 인터페이스는 아래와 같이 구현해서 사용한다.

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

메서드로 전달하기

if-else 대신 조건식을 프리디케이트 함수로 전달받아 수행하는 코드이다. 보다 유연하게 동작하는데다 가독성도 좋다.

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

다만, 메서드를 클래스로 감싸 전달하므로 코드가 불필요하게 중복된다.

다양한 활용방법

void를 반환하거나 String을 반환하는 프리디케이트를 구현할 수도 있다. 이를테면 컬렉션 객체를 검사해 출력할 메시지를 다르게 설정할 수 있겠다.

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

Java의 Predicate<T>boolean만을 반환하므로, 다양한 활용을 위해서는 직접 정의해서 사용해야 하겠다. 다만, 인터페이스에 두 개 이상 함수를 정의하면 @functionalInterface 어노테이션을 달았을 때 에러가 뜬다. 프리디케이트 인터페이스는 단 하나의 함수만을 가져야 한다.

2.2. 익명 클래스

2.1.의 AppleHeavyWeightPredicateAppleGreenColorPredicate는 함수를 전달하기만 한다. 아래와 같이 익명 클래스로 프리디케이트를 작성해 전달하면 보다 간결하게 표현할 수 있다.

List<Apple> redApples = filterApples(inventory, new ApplePredicate(){
    public boolean test(Apple a){
        return RED.equals(a.getColor());
    }
});

그러나 다음과 같이 객체 스코프가 복잡해지는 문제가 있다. 다음 코드의 결과값은 무엇일까?

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();
    }
}
정답 5

this 키워드

인스턴스 메서드 또는 생성자 안에서 this 키워드로 현재 객체를 참조할 수 있다. 지역 멤버 또는 매개변수로 상위 멤버가 가려지기 때문이다. this 키워드를 사용해 현재 객체의 모든 멤버를 참조할 수 있다.

또한 아래와 같이 생성자 코드 중복을 피하기 위해 사용할 수도 있다. 이 경우 this는 생성자의 첫 번째 행에서 호출되어야 한다.

public class Coordinate{
	public int x;
    public int y;
    
    public Coordinate(int x){
        this(x, 0);
    }
    
    public Coordinate(int y){
        this(0, y);
    }
    
    public Coordinate(int x, int y){
        this.x = x;
        this.y = y;
    }
}

출처 : Using the this Keyword (The Java™ Tutorials > Learning the Java Language > Classes and Objects) (oracle.com)

2.3. 람다 표현식

익명 클래스를 아래와 같이 더 간결하게 표현할 수 있다.

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

이러한 표현식을 람다라고 한다. 종류는 아래와 같이 두 가지가 있는데, 람다는 인자, 화살표, 몸체로 구성되어 있다. 구체적인 사용법은 3장에서 다룬다.

  • (인자) -> expression : 괄호 안 인자를 전달받아 화살표 뒤의 표현식을 수행하고 그 결과값을 반환한다는 의미이다. 표현식은System.out.println과 같이 void를 반환하는 경우도 있다.

  • (인자) -> { statements; } : 괄호 안 인자를 전달받아 화살표 뒤 중괄호 안의 문장을 수행한다는 의미이다. 반환값이 있다면 return을 사용해 명시적으로 표시해주어야 한다.

표현식(expression): 식을 처리하면 '하나의 값'을 반환. 이를테면 int variable = 0과 같은 할당식은 성공시 int를 반환하는 표현식이며, System.out.println(variable)은 Void를 반환하는 표현식이다.

문장(statements): 할당식, 변수 증감(++ --), 메서드 호출, 객체 생성, 제어문 등 수행할 작업의 한 단위이다. 일부 문장은 ; 없이 표현식으로 사용할 수도 있다.

출처 : Expressions, Statements, and Blocks (The Java™ Tutorials > Learning the Java Language > Language Basics) (oracle.com)

2.4. 제네릭으로 리스트 원소 형식을 추상화

제네릭을 사용해 리스트 원소 형식을 추상화한 경우이다.

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

정리 : 자바에서 동작을 파라미터화하는 방법

  • 프리디케이트 인터페이스를 구현한 객체를 전달
  • 익명 클래스로 감싸 함수를 전달
  • 람다식을 전달
  • 제네릭을 사용

결론, '동작'을 추상화해 매개인자로 '전달' 받음

⚠️ **GitHub.com Fallback** ⚠️