개발/Java

Java 제네릭 완벽 가이드

hanks 2026. 2. 22. 18:07

Java 제네릭의 개념, 사용 이유, 그리고 실제 코드 예시를 통해 제네릭을 완벽하게 이해하고 활용하는 방법을 알아봅니다.

왜 필요한가?

① 타입 안정성 문제

Java는 정적 타입 언어라서 컴파일 시 타입 검사를 수행하는데요, 제네릭이 없다면 타입 안정성을 보장하기 어려워지는 경우가 생겨요. 예를 들어 ArrayList에 여러 타입의 객체를 넣을 수 있다면, 나중에 꺼내 쓸 때 ClassCastException이 발생할 위험이 있죠.

java // 제네릭을 사용하지 않은 경우 ArrayList list = new ArrayList(); list.add("Hello"); list.add(123); // 컴파일 에러는 발생하지 않음

String str = (String) list.get(1); // 런타임 시 ClassCastException 발생!

② 코드 중복 문제

제네릭이 없다면 각 타입에 맞는 메서드를 일일이 만들어야 해서 코드 중복이 심해질 수 있어요. 예를 들어 Integer를 처리하는 메서드와 String을 처리하는 메서드를 따로 만들어야 하는 거죠.

java public Integer findMaxInteger(Integer[] arr) { Integer max = arr[0]; for (Integer i : arr) { if (i > max) { max = i; } } return max; }

public String findMaxString(String[] arr) { String max = arr[0]; for (String s : arr) { if (s.compareTo(max) > 0) { max = s; } } return max; }


핵심 개념

항목 설명 예시
타입 파라미터 클래스나 메서드를 정의할 때 타입을 일반화하는 변수 T, E, K, V
제네릭 클래스 타입 파라미터를 사용하여 정의된 클래스 ArrayList<T>
제네릭 메서드 타입 파라미터를 사용하여 정의된 메서드 <T> T findMax(T[] arr)
와일드카드 알 수 없는 타입을 나타내는 기호 ?
타입 제한 타입 파라미터에 허용되는 타입을 제한하는 것 <T extends Number>

[클라이언트] ── 타입 정보 지정 (예: String) ──→ [제네릭 클래스/메서드] ←── 지정된 타입으로 동작 ────┘


단계별 사용법

① 1단계: 제네릭 클래스 정의하기

제네릭 클래스는 클래스 이름 뒤에 <T>와 같이 타입 파라미터를 붙여서 정의합니다. 이 T는 클래스 내부에서 사용할 타입을 나타내는 일종의 placeholder 같은 건데요. 실제 사용할 때 구체적인 타입으로 지정해 줄 수 있어요.

java // 제네릭 클래스 정의 class Box { private T t;

public void set(T t) {
    this.t = t;
}

public T get() {
    return t;
}

}

// 제네릭 클래스 사용 Box integerBox = new Box<>(); integerBox.set(10); Integer value = integerBox.get(); // 타입 캐스팅 불필요 System.out.println(value);

Box stringBox = new Box<>(); stringBox.set("Hello"); String strValue = stringBox.get(); // 타입 캐스팅 불필요 System.out.println(strValue);

② 2단계: 제네릭 메서드 정의하기

제네릭 메서드는 메서드 선언 시 리턴 타입 앞에 <T>와 같이 타입 파라미터를 붙여서 정의합니다. 제네릭 클래스와 마찬가지로, 이 T는 메서드 내부에서 사용할 타입을 나타내죠.

java // 제네릭 메서드 정의 class Util { public static T findMax(T[] arr) { T max = arr[0]; for (T i : arr) { if (((Comparable) i).compareTo(max) > 0) { max = i; } } return max; } }

// 제네릭 메서드 사용 Integer[] intArr = {1, 5, 2, 8, 3}; Integer maxInt = Util.findMax(intArr); System.out.println("Max Integer: " + maxInt);

String[] strArr = {"apple", "banana", "cherry"}; String maxStr = Util.findMax(strArr); System.out.println("Max String: " + maxStr);

③ 3단계: 와일드카드 사용하기

와일드카드는 ? 기호를 사용하여 알 수 없는 타입을 나타낼 때 사용합니다. 와일드카드는 주로 메서드의 파라미터 타입으로 사용되는데요, 상한 경계(upper-bounded wildcard)와 하한 경계(lower-bounded wildcard)를 지정할 수도 있어요.

java // 와일드카드 사용 예시 public static void printList(List<?> list) { for (Object obj : list) { System.out.println(obj); } }

// 상한 경계 와일드카드 public static void printNumberList(List<? extends Number> list) { for (Number num : list) { System.out.println(num); } }

// 하한 경계 와일드카드 public static void addNumbers(List<? super Integer> list) { list.add(1); list.add(2); }

// 와일드카드 사용 List intList = Arrays.asList(1, 2, 3); printList(intList); printNumberList(intList);

List

objList = new ArrayList<>(); addNumbers(objList); System.out.println(objList);


자주 묻는 질문

Q: 제네릭 타입 소거(Type Erasure)란 무엇인가요?

A: Java 컴파일러는 제네릭 타입을 컴파일 시에만 검사하고, 런타임 시에는 제네릭 타입 정보를 제거합니다. 이를 타입 소거라고 하는데요. 타입 소거 때문에 런타임 시에는 제네릭 타입 정보를 알 수 없다는 점을 주의해야 해요.

Q: Raw Type은 왜 사용하면 안 되나요?

A: Raw Type은 제네릭 타입을 지정하지 않고 사용하는 것을 말하는데요, Raw Type을 사용하면 타입 안정성을 보장할 수 없고, 컴파일러 경고가 발생할 수 있어요. 따라서 제네릭 타입을 사용할 때는 항상 구체적인 타입을 지정하는 것이 좋습니다.


마무리

Java 제네릭은 타입 안정성을 높이고 코드 중복을 줄이는 데 매우 유용한 기능인데요. 제네릭 클래스, 제네릭 메서드, 와일드카드 등 다양한 기능을 활용하면 더욱 효율적인 코드를 작성할 수 있습니다.

  • 제네릭 클래스 정의 및 사용법 학습
  • 제네릭 메서드 정의 및 사용법 학습
  • 와일드카드 사용법 학습
  • 타입 제한 사용법 학습

개인적으로 제네릭을 처음 접했을 때는 좀 어렵게 느껴졌었는데, 실제 코드를 작성하면서 사용하다 보니 그 유용성을 체감하게 되더라고요. 특히 컬렉션 프레임워크를 사용할 때 제네릭은 필수적인 요소인 것 같아요. 앞으로도 제네릭을 적극적으로 활용해서 더 깔끔하고 안전한 코드를 작성해야겠어요.