[Java] Varargs에 대한 관찰

sania Ka·2021년 11월 21일
3

Java 기능 관찰

목록 보기
5/8

가변인자라고 부르는 varargs 또한 JDK 5에서 도입된 기능이다. 이 Varargs는 필요에 따라 매개변수의 개수를 가변적으로 조정할 수 있는 기능이다.

Varargs 사용법

가변인자 함수 생성 (code)

void varargsFunction(String... strings){
    for (String string : strings) {
        //Do Something
    }
}

가변인자 함수 사용 (code)

varargsFunction("A");
varargsFunction("A","B");
varargsFunction("A","B","C");

기본적으로 가변인자는 특정 함수의 오버로딩을 대체할 수 있다. 다음의 코드 예시를 보자.

파라미터의 개수만 다른 같은 이름의 함수 (code)

void varargsFunction(String string1){
    //Do Something
}
void varargsFunction(String string1, String string2){
    //Do Something
}
void varargsFunction(String string1, String string2, String string3){
    //Do Something
}
    ...

이렇게 작성하면 함수의 인자 개수가 달라질때마다 함수를 매번 새로 만들어야 한다.
개발자 입장에서 매우 비효율적인 구조이다. (그러나 JDK 9에서 추가된 Map.of() 메소드는 이 방법을 사용하고 있다. 그 이유는 아래서 설명한다.)

JDK 5 이전에서 Varargs를 처리하던 방법

모든 인자 개수에 대해 함수를 작성할 수는 없으므로, JDK 5 이전에는 일정 개수 이상의 인자를 처리할때는 보통 Collection이나 Array를 사용했다.

가변인자 없이 함수 생성 (code)

    void varargsFunction(String[] strings){
        for(int i = 0; i<strings.length; ++i){
            //Do Something
        }
    }

가변인자 없이 함수 사용 (code)

String[] arr1 = {"A"};
varargsFunction(arr1);

Varargs before vs after

가변인자가 가장 많이 사용되는 곳은 보통 String formatting일 것이다. 그러나 String.formatPrintStream.printf 함수 자체가 이 Varargs와 함께 JDK 5에서 추가된 함수이다. 그래서 그 이전에는 MessageFormat.format() 을 사용했다.

Without varargs (code)

Object[] arguments = {new Integer(7), new Date(), "a disturbance in the Force"};

String result = MessageFormat.format("At {1,time} on {1,date}, there was {2} on planet {0,number,integer}.", arguments);

JDK 5 이전에는 서로 다른 인자를 넘겨주기 위해서 Object 배열을 생성하고, 데이터를 집어넣은 뒤 이 배열을 넘기는 방식으로 처리했다.

With varargs (code)

String result = MessageFormat.format(
        "At {1,time} on {1,date}, there was {2} on planet {0,number,integer}.", 
        7, new Date(), "a disturbance in the Force"
    );

단순하게 보더라도 코드의 길이가 확연하게 줄어든 것을 알 수 있다.
그렇다면 어떻게 이 Varargs가 동작하는 것일까?

Varargs의 동작 원리

컴파일러가 컴파일 타임에 배열을 만들고 그 배열에 데이터를 넣는 코드를 삽입해주는게 다이다. 이것은 바이트 코드를 확인하면 쉽게 확인해 볼 수 있다.

자바코드
위의 자바코드는 아래의 바이트 코드로 치환된다.
바이트 코드
바이트 코드를 보면 ANEWARRAY 명령을 통해 Object 배열을 생성하고, 차례대로 "A", 1, 8.0을 삽입한 뒤, 이 배열을 함수 인자로 넘겨주는것에 불과하다.

여기서 한가지 의문이 생긴다. 함수를 실행할 때 배열을 인자로 넘겨주는데, 함수가 실행될 때도 배열 형태로 실행되는 것일까?

당연하게도, Object... args 형태로 선언된 파라미터는 Object[] args와 완벽히 동일하게 사용할 수 있다.
실제로 바이트코드상에서 보더라도 인자가 배열로 바뀌어있다. 그래서 가변 인자가 선언된 함수에서는 해당 인자를 배열과 동일하게 다룰 수 있다.

다만, Object[] args로 선언된 경우에는 컴파일러가 자동으로 배열을 생성해 주지 않아서, 개발자가 직접 배열을 만들어서 인자로 넘겨야 한다는 차이점이 있다.

Varargs를 사용하면서 주의해야할 점

Varargs로 선언된 함수를 호출하면, 컴파일러가 배열을 함수 실행 직전에 생성하도록 코드를 변경하는데, 이것이 문제가 될 수 있다.
특정 메서드를 실행하는데 인자의 개수가 일정 이상으로 증가할 필요가 없다면, 단순하게 인자의 개수가 다른 메서드를 오버로딩으로 생성하는게 성능상으로 더 좋다.
특히, 반복문 안에서 실행하는 경우에는 매 실행시마다 배열을 만들어야 하기 때문에, 성능문제가 더 눈에 띈다.

JDK 9에서 추가된 Map.of 메서드가 인자 0~10개까지의 함수 오버로딩으로 생성되어 있는 이유도 동일하다.

Generic과 함께 사용하기

Generic 또한 JDK5에서 추가되었기 때문에, Generic과 Varargs를 함께 사용 할 수 있다.

Generic with Varargs (code)

<T> void varargsFunction(T... strings){
    //Do Something
}

static void varargsFunction2(List<String>... data){
    //Do Something
}

그러나 이러한 형태는 타입 안정성이 보장이 되지 않기 때문에 별로 권장되지 않는다.

Generic과 함께 사용할 때 주의해야할 점

Generic에 관한 관찰-1에서 살펴봤지만, 컴파일 타임에 제네릭은 타입 데이터가 제거되어 Object로 변환되며, 필요시에 캐스팅 연산을 삽입한다. 마찬가지로 제네릭 Varargs는 Object 배열로 변환되고 필요한 경우에 캐스팅한다.
제네릭과 마찬가지로, 이 Varargs또한 컴파일 타임에 실행되기 때문에, 제네릭에 대한 컴파일러의 도움을 받을수 없다는 것이 문제가 된다.

Heap pollution

{
    String a = "A";
    String b = "B";
    String c = "C";
    varargsFunction(a,b,c);
}

<T> void varargsFunction(T... strings){
    Object[] arr = strings;
    arr[2] = 10;
}

위와 같은 코드를 작성했을 때, 정상적인 코드가 아님에도 정상적으로 컴파일이 되는 것을 확인 할 수 있다. (물론 위 코드는 실행했을때는 RuntimeException이 발생한다.) 분명 잘못된 코드임에도 불구하고, 컴파일러는 그 어떠한 경고 조차 할 수 없다.

Heap pollution 해결

이 문제를 해결하기 위해서는 제네릭에 타입제한을 걸어 함수를 사용하는 방법이 권장된다.

<T extends String> void varargsFunction(T... strings){
    String[] arr = strings;
    arr[2] = 10;
}

위처럼 작성하는 경우, T가 Object가 아닌 String으로 취급되기 때문에 컴파일 타임에 문제점을 미리 알아낼 수 있다.

Heap pollution with list (code)

<T extends List<String>> void varargsFunction(T... strings){
    Object[] arr = strings;
    List<Integer> integers = new ArrayList<Integer>();
    integers.add(50);
    arr[2] = integers;
    System.out.println(strings[2].get(0));
}


위의 코드는 제네릭과 가변인자에 대한 규칙에 따라 List<String>...List[]로 변경된다. 그리고 System.out.println(strings[2].get(0))에 캐스팅 연산을 삽입한다.

컴파일 타임에는 문법에 문제가 있는 것이 아니므로 오류가 발생하지 않고, 런타임에 ClassCastException이 발생하게 된다.

만약 출력을 하지 않고, arr를 상위 메서드에 리턴한다면, 해당 메서드 또한 정상적이지 않은 동작을 할 가능성이 높아진다. 그러므로 제네릭과 가변인자를 동시에 사용하는 경우에는 매우 조심하여 사용해야 한다.

위의 코드는 타입 한정 제네릭을 사용하므로, Object[]가 아닌 List<String>[]를 사용하면 안전하게 사용 할 수 있다. (안전하게 컴파일 에러가 발생한다.)
Heap pollution 위키피디아 문서


SafeVarargs

JDK 7 이전에는 제네릭 가변인자 메서드를 사용하는 경우 매번 경고가 발생했고, 이 경고를 그냥 방치하거나 @SuppressWarnings("unchecked") 어노테이션을 달아 경고를 숨겼다. 그러나 JDK 7 이후 @SafeVarargs 어노테이션을 제네릭 가변인자 메서드에 선언하여 경고가 보이지 않도록 할 수 있게 되었다. 물론, 이 어노테이션이 붙은 메서드는 타입 안정성이 보장되어야만 한다.
SafeVarargs 가이드

0개의 댓글