53. 가변인수는 신중히 사용하라
📝 아이템 53: 가변인수는 신중히 사용하라
🔹 핵심 요약
✅ 가변인수(varargs)는 메서드에 가변 개수의 인자를 전달할 수 있는 편리한 기능 ✅ 내부적으로 배열을 생성하므로 성능상 이슈가 발생할 수 있음 ✅ 빈 배열이나 null이 들어올 수 있는 상황을 주의해야 함 ✅ 가변인수와 제네릭을 함께 사용할 때 주의가 필요함
📚 필수 개념 정리
💡 가변인수(Varargs)
Java 5부터 도입된 기능으로 메서드에 여러 개의 인자를 전달할 수 있게 함
가변인수는 메서드 선언 시 타입 뒤에
...
을 붙여 선언내부적으로는 배열로 처리됨
메서드 매개변수 중 가장 마지막에 위치해야 함
public void printNumbers(int... numbers) {
for (int number : numbers) {
System.out.print(number + " ");
}
System.out.println();
}
// 호출 방법
printNumbers(1, 2, 3); // 여러 인자 전달
printNumbers(); // 인자 없이도 호출 가능
printNumbers(new int[]{4, 5, 6}); // 배열로도 전달 가능
💡 가변인수 동작 방식
컴파일러는 가변인수를 배열로 변환하여 처리
메서드가 호출될 때마다 새로운 배열 생성
위 예시의
printNumbers(1, 2, 3)
는 내부적으로new int[]{1, 2, 3}
으로 변환됨
🚨 가변인수의 문제점
📦 성능 이슈
public class PerformanceTest {
/**
* 가변인수를 사용한 메서드 - 매 호출마다 배열 생성
*/
public static int sum(int... numbers) {
int sum = 0;
for (int num : numbers) {
sum += num;
}
return sum;
}
public static void main(String[] args) {
// 매 호출마다 새 배열 생성 (메모리 오버헤드)
for (int i = 0; i < 1000; i++) {
sum(1, 2, 3, 4, 5); // 매번 길이 5의 배열 생성
}
}
}
👉 가변인수 메서드는 호출될 때마다 배열을 새로 할당하고 초기화하므로, 성능에 민감한 상황에서는 주의 필요
🤔 고정 매개변수도 같은 크기의 메모리를 쓰는데, 왜 배열만 문제가 될까?
힙 메모리 할당: 가변인수는 매 호출마다 힙에 새 배열 객체 생성
가비지 컬렉션 부담: 임시 배열은 메서드 종료 후 배열은 즉시 가비지가 되어 GC에 부담
객체 생성 오버헤드: 생성되는 배열 객체는 단순한 데이터 저장 외에도 객체 헤더를 포함 (클래스 메타데이터 포인터, 해시코드, 동기화 정보 등)
메모리 단편화: 빈번한 배열 할당/해제는 힙 메모리 단편화 유발
CPU 캐시 효율 저하: 예측 불가능한 메모리 접근 패턴으로 캐시 효율성 감소
🛡️ 안전성 이슈
public class SafetyIssue {
/**
* 배열의 최솟값을 찾는 메서드
* @param values 정수 배열
* @return 배열의 최솟값
* @throws IllegalArgumentException 배열이 비어있을 경우
*/
public static int min(int... values) {
if (values.length == 0) {
throw new IllegalArgumentException("인자가 1개 이상 필요합니다");
}
int min = values[0];
for (int i = 1; i < values.length; i++) {
if (values[i] < min) {
min = values[i];
}
}
return min;
}
public static void main(String[] args) {
min(); // 실행 시 예외 발생: IllegalArgumentException
}
}
👉 가변인수는 인자가 없어도 메서드 호출이 가능하므로, 빈 배열에 대한 처리 필요
🧩 제네릭과 가변인수의 조합
public class GenericVarargs {
/**
* 제네릭 타입의 가변인수를 사용한 메서드
* @param elements 가변 개수의 제네릭 요소
* @return 입력 요소를 포함하는 리스트
*/
@SafeVarargs // 경고 억제 애너테이션
public static <T> List<T> asList(T... elements) {
// 제네릭은 직접 배열(new T[])을 만들 수 없음
// 하지만 가변인수는 호출 시점에 컴파일러가 실체화된 배열 (예: String[])을 생성해 전달하므로 사용 가능
List<T> result = new ArrayList<>();
for (T element : elements) {
result.add(element);
}
return result;
}
public static void main(String[] args) {
// 제네릭 가변인수 사용
List<String> list = asList("가", "나", "다");
System.out.println(list); // [가, 나, 다]
}
}
⚠️ 제네릭 배열 생성 불가 문제: 실체화 불가 타입으로 배열을 만들 수 없는데, 가변인수는 내부적으로 배열 생성
⚠️ 컴파일러 경고 발생: 타입 안전성을 보장할 수 없어 경고 발생, @SafeVarargs
로 억제 가능
🧪 해결책
🛠️ 성능 최적화: 오버로딩 활용하기
자주 사용되는 인자 개수에 대해 별도 메서드를 제공하고, 나머지 경우만 가변인수 메서드로 처리
public class OptimizedVarargs {
// 0개, 1개, 2개 인자에 대한 특별 메서드
public static int sum() {
return 0;
}
public static int sum(int a) {
return a;
}
public static int sum(int a, int b) {
return a + b;
}
// 3개 이상의 인자는 가변인수로 처리
public static int sum(int a, int b, int c, int... rest) {
int result = a + b + c;
for (int i : rest) {
result += i;
}
return result;
}
public static void main(String[] args) {
System.out.println(sum()); // 배열 생성 없음
System.out.println(sum(1)); // 배열 생성 없음
System.out.println(sum(1, 2)); // 배열 생성 없음
System.out.println(sum(1, 2, 3)); // 빈 배열 생성
System.out.println(sum(1, 2, 3, 4, 5)); // 길이 2 배열 생성
}
}
👉 자주 사용되는 케이스에 대해서는 배열 생성 비용 절약
🔒 안전한 가변인수 설계
public class SafeVarargs {
/**
* 최솟값을 찾는 메서드의 안전한 구현
* @param first 첫 번째 값 (필수)
* @param rest 나머지 값들 (선택)
* @return 최솟값
*/
public static int min(int first, int... rest) {
int min = first;
for (int i : rest) {
if (i < min) {
min = i;
}
}
return min;
}
public static void main(String[] args) {
System.out.println(min(10)); // OK: 10
System.out.println(min(3, 1, 4, 1, 5)); // OK: 1
// min(); // 컴파일 에러: 최소 하나의 인자 필요
}
}
👉 필수 매개변수를 별도로 분리하여 빈 인자 문제 해결
🛡️ 제네릭 가변인수 안전하게 사용하기
public class SafeGenericVarargs {
/**
* 안전한 제네릭 가변인수 메서드
* @param elements 요소들
* @return 요소들을 포함하는 불변 리스트
*/
@SafeVarargs // 타입 안전성 보장
public static <T> List<T> of(T... elements) {
// 가변인수 배열의 참조를 외부로 노출하지 않음
return Collections.unmodifiableList(Arrays.asList(elements));
}
// 안전하지 않은 예: 가변인수로 생성된 배열을 노출
// public static <T> T[] toArray(T... elements) {
// return elements; // 위험! 가변인수 배열을 그대로 반환
// }
public static void main(String[] args) {
List<String> list = of("안전한", "제네릭", "가변인수");
System.out.println(list); // [안전한, 제네릭, 가변인수]
}
}
👉 제네릭 가변인수 사용 시 안전한 방법:
가변인수 배열에 아무것도 저장하지 않기
배열의 참조를 외부로 노출하지 않기
@SafeVarargs 애너테이션으로 의도 명시하기
📋 가변인수 활용 예시
📝 문자열 연결
public class StringJoiner {
/**
* 여러 문자열을 지정된 구분자로 연결
* @param delimiter 구분자
* @param strings 연결할 문자열들
* @return 연결된 문자열
*/
public static String join(String delimiter, String... strings) {
if (strings.length == 0) {
return "";
}
StringBuilder result = new StringBuilder(strings[0]);
for (int i = 1; i < strings.length; i++) {
result.append(delimiter).append(strings[i]);
}
return result.toString();
}
public static void main(String[] args) {
System.out.println(join(", ", "사과", "바나나", "오렌지"));
// 출력: 사과, 바나나, 오렌지
System.out.println(join(" - ", "Java", "Spring", "JPA"));
// 출력: Java - Spring - JPA
}
}
🎯 결론
📍 가변인수는 메서드에 가변 개수의 인자를 전달할 때 유용하지만, 성능과 안전성 문제에 주의해야 함
📍 매번 호출마다 배열이 생성되므로 성능에 민감한 상황에서는 오버로딩을 통해 최적화
📍 빈 배열이 생성될 수 있는 상황을 고려하여 방어적으로 코딩
📍 정말 필요한 상황에서만 가변인수를 사용하고, 남용하지 말 것
✨ "가변인수는 편리하지만, 언제나 신중하게!" 이 아이템의 핵심이다.
💭 느낀 점
💡 가변인수는 정말 편리하지만, 내부적으로 배열이 생성된다는 사실을 항상 염두에 두고 성능에 민감한 상황에서는 주의해서 사용해야 함을 이해함
💡 자주 호출되는 메서드에서 가변인수를 사용할 경우, 수많은 배열이 생성되어 성능 저하로 이어질 수 있다는 점이 인상적
💡 자바의 타입 시스템과 가변인수의 내부 동작을 제대로 이해하는 것이 성능 최적화에 있어 중요함을 느낌
Last updated