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) ──→ [제네릭 클래스/메서드] ←── 지정된 타입으로 동작 ────┘
제네릭 클래스는 클래스 이름 뒤에 <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);
제네릭 메서드는 메서드 선언 시 리턴 타입 앞에 <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);
와일드카드는 ? 기호를 사용하여 알 수 없는 타입을 나타낼 때 사용합니다. 와일드카드는 주로 메서드의 파라미터 타입으로 사용되는데요, 상한 경계(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
A: Java 컴파일러는 제네릭 타입을 컴파일 시에만 검사하고, 런타임 시에는 제네릭 타입 정보를 제거합니다. 이를 타입 소거라고 하는데요. 타입 소거 때문에 런타임 시에는 제네릭 타입 정보를 알 수 없다는 점을 주의해야 해요.
A: Raw Type은 제네릭 타입을 지정하지 않고 사용하는 것을 말하는데요, Raw Type을 사용하면 타입 안정성을 보장할 수 없고, 컴파일러 경고가 발생할 수 있어요. 따라서 제네릭 타입을 사용할 때는 항상 구체적인 타입을 지정하는 것이 좋습니다.
Java 제네릭은 타입 안정성을 높이고 코드 중복을 줄이는 데 매우 유용한 기능인데요. 제네릭 클래스, 제네릭 메서드, 와일드카드 등 다양한 기능을 활용하면 더욱 효율적인 코드를 작성할 수 있습니다.
개인적으로 제네릭을 처음 접했을 때는 좀 어렵게 느껴졌었는데, 실제 코드를 작성하면서 사용하다 보니 그 유용성을 체감하게 되더라고요. 특히 컬렉션 프레임워크를 사용할 때 제네릭은 필수적인 요소인 것 같아요. 앞으로도 제네릭을 적극적으로 활용해서 더 깔끔하고 안전한 코드를 작성해야겠어요.
| Java 컬렉션 프레임워크 비교 (0) | 2026.02.22 |
|---|---|
| Java 멀티스레드 동기화 (0) | 2026.02.22 |
| Java 람다 표현식 정리 (0) | 2026.02.22 |
| Java Optional 사용법 (0) | 2026.02.22 |
| Java Stream API 활용법 (0) | 2026.02.22 |