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);  // [안전한, 제네릭, 가변인수]
    }
}

👉 제네릭 가변인수 사용 시 안전한 방법:

  1. 가변인수 배열에 아무것도 저장하지 않기

  2. 배열의 참조를 외부로 노출하지 않기

  3. @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