가변 인자란?
가변 인자란, Java 5 이후로 도입되었으며, 임의의 수를 가진 파라미터를 받을 때 유용하게 사용할 수 있다.
public int adder(int a1, int a2, int a3) {
return a1 + a2 + a3;
}
파라미터가 3개라면 위와 같은 덧셈 계산기를 만들 수 있다.
100개의 인자를 더하고 싶다면 어떻게 할까? 파라미터가 100개인 adder 메소드를 오버로딩할까?
파라미터가 10000개가 된다면?
이 시점에서 생각해볼 수 있는 것이, 배열 혹은 리스트이다.
배열 또는 리스트로 값을 받을 수만 있다면 iterator로 반복 접근도 가능하며, 오버로딩에 대해 걱정할 필요가 없다.
따라서 Varargs(가변 인자)를 사용한다.
public int adder(int... numbers) {
int sum = 0;
for (int element : numbers) {
sum += element;
}
return sum;
}
이처럼 가변 인자를 사용하면 여러 파라미터를 한번에 다룰 수 있다.
이 시점에서 궁금해지는 것은, '어떻게 이런 것이 가능할까?' 이다.
가변 인자의 원리
앞에서 가변 인자를 사용할 경우, iterator를 통한 접근이 가능하다고 했다.
유추할 수 있듯이, 가변인자는 JVM에 의해 컴파일 타임에 배열로 변환된다.
즉, 다음 두 코드는 거의 같다.
public int adder(int... numbers) {
int sum = 0;
for (int element : numbers) {
sum += element;
}
return sum;
}
// 배열 버전
public int adder(int[] numbers) {
int sum = 0;
for (int element : numbers) {
sum += element;
}
return sum;
}
인자로 넘겨주는 값이 array로 매핑되고, 이를 가변인자를 사용하는 메소드에서 사용할 수 있게 된다.
다만, 배열로 정의한 메소드의 경우 인자를 무조건 배열로 passing 해야 하며, 0개의 인자는 허용되지 않는다.
따라서 두 메서드는 완전히 동일하지는 않다.
사용 규칙
가변인자에 대한 사용 제약조건이 많은 것은 아니지만, 몇가지 사용 규칙이 존재한다.
- 메소드는 하나의 가변인자만 가질 수 있다.
public int adder(int... x, int... y) {
...
}
가변인자의 경우 선언된 시점부터 뒤로 들어오는 모든 인자가 해당 가변인자의 일부로 인식된다.
따라서 가변인자를 두 개 이상 사용하는 것은 불가능하다.
- 가변인자는 마지막 파라미터로만 존재할 수 있다.
public int adder(int... x, int y) {
...
}
앞서 말했던 '하나의 가변인자만 가질 수 있다'와 마찬가지의 이유이다.
가변인자 뒤에 선언되는 당연히 파라미터는 무시되게 되므로 사용할 수 없다.
성능
가변인자는 인자들을 내부적으로 배열로 변환시키기에 성능 상의 단점도 존재한다.
List에서 제공하는 List.of 와 같은 정적 메소드는 다음과 같이 가변인수를 정말 최후의 수단으로만 사용한다.
static <E> List<E> of() {
return ImmutableCollections.emptyList();
}
static <E> List<E> of(E e1) {
return new ImmutableCollections.List12<>(e1);
}
...
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9, E e10) {
return new ImmutableCollections.ListN<>(e1, e2, e3, e4, e5,
e6, e7, e8, e9, e10);
}
@SafeVarargs
@SuppressWarnings("varargs")
static <E> List<E> of(E... elements) {
switch (elements.length) { // implicit null check of element
case 0:
return ImmutableCollections.emptyList();
case 1:
return new ImmutableCollections.List12<>(elements[0]);
case 2:
return new ImmutableCollections.List12<>(elements[0], elements[1]);
default:
return new ImmutableCollections.ListN<>(elements);
}
}
힙 오염(Heap Pollution)
가변인자는 메소드에게 유연성을 제공하는 것 같다.
덕분에 인자가 몇 개 인지 확실하지 않은 상태에서도 메소드를 쉽게 작성할 수 있게 되었다.
그러면, 가변인자는 단점은 없는 이상적인 기술인가?
치명적인 단점을 하나 가지고 있다. 그건 바로 힙 오염이다.
우선 힙(heap)이란?
참조형의 데이터 타입을 갖는 객체(인스턴스), 배열 등이 저장되는 메모리 공간이다.
즉, JVM의 힙 공간이 오염되었다는 의미이다.
왜 오염되는지에 대해서 이해하려면, 제네릭 타입에 대한 이해가 선행되어야 한다.
Java에 제네릭이 도입될 당시, 기존 코드와의 호환성을 위해 타입 파라미터를 컴파일이 끝난 시점에 제거하도록 만들었다.
즉, 다음과 같은 두 리스트는 컴파일이 끝난 시점에서 동일하게 변한다.
ArrayList<Integer> integers = new ArrayList<>();
ArrayList<String> strings = new ArrayList<>();
// 컴파일 후
ArrayList<Object> integers = new ArrayList<>();
ArrayList<Object> strings = new ArrayList<>();
제네릭이 도입되기 이전에 사용되던 ArrayList가 Object 타입을 담는 자료구조였기 때문이다.
따라서 기존 ArrayList와의 하위 호환성을 보장하면서, 제네릭을 사용하기 위해서는 위와 같은 형태가 될 수 밖에 없다.
이처럼 컴파일 타임에 타입 소거가 되는 타입들을 '실체화 불가 타입'이라고 한다.
private static void doSomthing(List<String> ... stringLists) {
List<Integer> intList = Arrays.asList(42);
Object[] objects = stringLists;
objects[0] = intList; // 힙 오염 발생
String s = stringLists[0].get(0); // ClassCastException
}
이제 위 코드를 되돌아가보자.
가변 인자로 String 타입의 List를 가진 배열을 만들어냈다.
하지만 앞서 말했듯, 제네릭은 컴파일 타임에 모두 타입 소거가 진행된다.
private static void doSomthing(List<Object> ... stringLists) {
List<Object> intList = Arrays.asList(42);
Object[] objects = stringLists;
objects[0] = intList; // 힙 오염 발생
String s = stringLists[0].get(0); // ClassCastException
}
따라서 위처럼 제네릭의 경우 Object로 타입이 모두 변경된다.
따라서 3번째 라인에서 objects의 0번째 인덱스를 Integer List로 변환해도 컴파일러는 눈치챌 수 없다.
두 리스트 모두 Object 타입의 리스트인데, 무슨 문제가 있는건가? 라는 생각을 가질 것이다.
따라서 이때까지도 컴파일러는 알 수 없고,
이 시점에서 해당 배열에는 두 타입(String, Integer)의 리스트가 공존하게 된다. 이것이 힙 오염(Heap Pollution)이다.
힙 오염을 알아차리는 시점은 런타임에 4번째 라인처럼 직접 접근했을 때 뿐이다.
이 때는 Integer 리스트에서 String 타입의 객체를 얻으려 하므로 ClassCastException이 발생한다.
그럼에도 가변인수는 유용하다
가변인수는 실체화 불가 타입(제네릭)으로 매개변수를 선언할 수 있게 되니, 타입 안정성이 깨지게 된다.
사실 이는 가변인수만의 문제가 아니라, 제네릭의 근본적인 문제점이다.
하지만 가변인수에서 위험성이 더 증대되는 이유는, 매개변수를 Object 타입의 배열로 다루기 때문이다.
가변인수가 이런 위험성을 가지고 있는데도 사용하는 이유는 코드에 극도의 유연함을 제공하기 때문이다.
그렇다고 해서 가변인수를 사용할 때, 아무런 안전장치가 없는 것은 아니다.
힙 오염의 가능성이 존재할 때에는, 컴파일러가 경고를 표시해준다.
이러한 경고를 제거하기 위해서는 Java 1.7 이후로 도입된 @SafeVarags 어노테이션을 사용하면 된다.
다만 가변인수를 사용하는 메소드가 안전한 것이 확인된 게 아니라면 절대 @SafeVarags를 작성하면 안된다.
이처럼 trade-off가 존재하는 가변인수지만, 단점을 알고 상황에 맞춰서 잘 쓰면 극도의 유연성을 제공할 것이다.
참고자료
https://parkadd.tistory.com/130
https://velog.io/@adduci/Java-%ED%9E%99-%ED%8E%84%EB%A3%A8%EC%85%98-Heap-pollution
'Java' 카테고리의 다른 글
Java: Throwable 소개 & API (0) | 2023.03.07 |
---|---|
Java: enum의 구현방식 알아보기 (8) | 2023.03.06 |
Java: enum 소개 및 API 파헤쳐 보기 (3) | 2023.03.05 |
Java: equals & hashCode (0) | 2023.03.04 |
Java: 동일성과 동등성 (2) | 2023.02.25 |