Daily log

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

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

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

'개발 > 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

공유하기

facebook twitter kakaoTalk kakaostory naver band