[Java] 런타임에 메서드를 전달한다는게 무슨 말일까?

2023. 7. 17. 01:31☕️ Java

모던 자바 인 액션 초반 부엔 일급 객체가 등장한 배경을 설명하면서 아래와 같은 구절이 나온다.

만약 런타임에 메서드를 전달할 수 있다면, 즉 메서드를 일급 시민으로 만들 수 있으면 프로그래밍에 유용하게 활용할 수 있다. (넹?)

 

 

문맥상, 일급 객체(시민)를 메서드의 인자로 할당할 수 있어 생기는 이점에 대해 설명하는 내용이지만, 런타임에 메서드를 전달한다는 말이 쉽게 와닿지 않는다. 

 

런타임 메서드 전달이 무슨 의미일까?

우선 런타임은 문자 그대로 프로그램이 실행되는 시간을 의미하는데, 이미 애플리케이션이 실행된 시점에 메서드를 객체와 연동하여 전달하고 호출하는 기법을 의미한다. 프로그래밍의 람다 표현식이나 콜백 함수, 디자인 패턴에선 전략패턴 등 여러 방법으로 구현할 수 있다.

이러한 기법의 공통점은 바로 애플리케이션이 실행되는 시점에 메서드의 동작을 변경하는데 사용된다는 점이다. 그럼 애플리케이션이 실행되기 전엔 메서드의 동작을 변경하지 않는다는 의미인데 코드를 예시로 보자. 

 

// Arrays.java
public static <T> void sort(T[] a, Comparator<? super T> c) {
        if (c == null) {
            sort(a);
        } else {
            if (LegacyMergeSort.userRequested)
                legacyMergeSort(a, c);
            else
                TimSort.sort(a, 0, a.length, c, null, 0, 0);
        }
    }

 

sort() 는 Arrays 의 정적 메서드로 선언된 메서드다. sort() 의 두번째 인자는 Comparator 타입으로 변수 c 는 코드를 몇 번이나 타고타고 들어간 후에야 Comparator 의 compare() 메서드를 호출하며 정렬을 수행한다. 그럼 이 변수 c 의 compare() 메서드는 과연 누가 구현하는가? 바로 어떤 식으로 정렬하고 싶은지 구현을 결정할 개발자에게 책임이 있다. 아래 코드에선 람다 표현식을 이용해 Integer 배열을 오름차순 정렬했다.

 

public class Main {
    
    public static void main(String[] args) {
        Integer[] numbers = {3, 1, 4, 2, 5};

        // 람다 표현식을 사용한 메서드 전달
        Comparator<Integer> ascendingComparator = (o1, o2) -> o1.compareTo(o2);
        Arrays.sort(numbers, ascendingComparator);

    }
}

 

만약 내림차순으로 정렬을 구현하고 싶다면 어떻게 할까? 아래처럼 구현하면 된다.

 

public class Main {
    
    public static void main(String[] args) {
        Integer[] numbers = {3, 1, 4, 2, 5};

        // 오름차순
        Comparator<Integer> ascendingComparator = (o1, o2) -> o1.compareTo(o2);
        
        // 내림차순
        Comparator<Integer> descendingComparator = (o1, o2) -> o2.compareTo(o1);
        
        Arrays.sort(numbers, ascendingComparator);
        Arrays.sort(numbers, descendingComparator);

    }
}

 

여기서 하고 싶은 말은 내림차순을 구현하는 코드가 아니라, 바로 런타임 시점에 어떻게 메서드가 전달된다는 것이다.

즉 Comparator<Integer> 를 구현하는 두 람다식은 이미 컴파일 타임에 선언돼 있을지라도, 이를 실행하는 sort() 메서드는 각기 다른 람다식을 런타임에야 전달받는 것이다. sort() 메서드의 구현을 다시한번 살펴보자. Comparator 타입 c 는 또 한번 메서드의 인자로 전달될 뿐 구체적인 구현이 정의되지 않은 상태다. 즉 런타임에 어떤 내용으로 구현된 Comparator c 가 전달되느냐에 따라 정렬 결과가 달라지는 것을 알 수 있다.

// Arrays.java
public static <T> void sort(T[] a, Comparator<? super T> c) {
        if (c == null) {
            sort(a);
        } else {
            if (LegacyMergeSort.userRequested)
                legacyMergeSort(a, c);
            else
                TimSort.sort(a, 0, a.length, c, null, 0, 0);
        }
    }

 

다시 처음의 질문을 되돌아보자. 

만약 런타임에 메서드를 전달할 수 있다면, 즉 메서드를 일급 시민으로 만들 수 있으면 프로그래밍에 유용하게 활용할 수 있다!

그렇다. Comparator 같은 함수형 인터페이스를 인자로 받는 sort() 메서드는, 런타임이 되어서야 자신(sort)이 어떤 메서드를 호출할지 알 수 있다.

즉 런타임에 전달할 수 있는 메서드, 메서드를 일급 객체로 활용할 수 있으면 프로그래밍에 유용하게 활용할 수 있다는 말은 아래 예시와 같다. 

List<Apple> filterApples(List<Apple> inventory, Predicate<Apple> p) {
    return inventory.stream()
        .filter(p)
        .collect(toList());
}


@Test
public void test() {
    List<Apple> 초록사과 = filterApples(사과박스, a -> a.getColor().equals(GREEN));
    List<Apple> 무거운사과 = filterApples(사과박스, a -> a.getWeight() >= 100);
    List<Apple> 상급사과 = filterApples(사과박스, a -> a.getQuality().equals("Good"));

    초록사과.forEach(i -> assertEquals(i.getColor(), GREEN));
    무거운사과.forEach(i -> assertTrue(i.getWeight() >= 100));
    상급사과.forEach(i -> assertEquals(i.getQuality(), "high"));
}

 

filterApples() 메서드는 Predicate p 를 인자로 받지만 정작 컴파일 타임에는 어떤 형태의 p 가 전달될지 알 수 없다.

test 코드에 개발자가 람다식을 써두긴 했지만, 이 람다식은 런타임, 즉 실행이 된 후에야 filter 메서드가 호출할 수 있다. 런타임에 메서드를 전달한다는 것은 위 같은 개념이며 일급객체(람다식)를 활용하여 좀 더 유연하게 필터링 메서드를 구현할 수 있는걸 확인할 수 있다.