[Java 개념노트 52] 제네릭이란 무엇인가?
안녕하세요. Java 개념노트 시리즈 쉰두 번째 글입니다.
지난 글에서는 컬렉션의 값을 하나씩 꺼내는 Iterator와 컬렉션 반복 방법에 대해 정리했습니다. 이번 글에서는 컬렉션을 사용할 때 계속 등장했던 제네릭에 대해 알아보겠습니다.
우리는 이미 ArrayList<String>, HashSet<Integer>, HashMap<String, Integer> 같은 코드를 여러 번 봤습니다. 여기서 꺾쇠괄호 < > 안에 들어가는 타입이 바로 제네릭과 관련된 부분입니다.
1. 제네릭이란?
제네릭은 클래스나 메서드에서 사용할 데이터 타입을 미리 정해두는 문법입니다.
컬렉션을 예로 들면, ArrayList 안에 문자열만 넣을지, 정수만 넣을지, 직접 만든 객체만 넣을지 정할 수 있습니다. 이렇게 타입을 정해두면 잘못된 타입의 값이 들어가는 것을 막을 수 있습니다.
쉽게 말하면
제네릭은 “이 상자에는 어떤 타입의 값만 넣을 수 있다”라고 미리 정해두는 문법입니다.
2. 제네릭을 사용하는 기본 형태
제네릭은 보통 꺾쇠괄호 < > 안에 타입을 넣어서 사용합니다.
ArrayList<String> names = new ArrayList<>();
위 코드에서 <String>은 이 ArrayList에 문자열만 저장하겠다는 뜻입니다.
| 코드 | 의미 |
ArrayList<String> |
String만 저장하는 ArrayList |
HashSet<Integer> |
Integer만 저장하는 HashSet |
HashMap<String, Integer> |
String key와 Integer value를 저장하는 HashMap |
3. 제네릭이 필요한 이유
제네릭이 필요한 가장 큰 이유는 타입 안정성입니다.
타입 안정성이란 잘못된 타입의 값이 들어가는 것을 미리 막아주는 것을 말합니다. 예를 들어 문자열만 저장해야 하는 목록에 숫자가 들어가면 나중에 문제가 생길 수 있습니다. 제네릭을 사용하면 이런 실수를 컴파일 단계에서 막을 수 있습니다.
ArrayList<String> names = new ArrayList<>();
names.add("Java");
names.add("Spring");
// names.add(100); // 오류 발생
names는 String만 저장하는 ArrayList입니다. 따라서 정수 100은 추가할 수 없습니다.
핵심 포인트
제네릭을 사용하면 잘못된 타입의 값이 들어가는 것을 미리 막을 수 있습니다.
4. 제네릭이 없으면 생기는 문제
제네릭을 사용하지 않으면 여러 타입의 값이 섞여 들어갈 수 있습니다.
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add("Java");
list.add(100);
String text = (String) list.get(0);
String number = (String) list.get(1);
System.out.println(text);
System.out.println(number);
}
}
위 코드는 실행 중 문제가 생길 수 있습니다. list.get(1)은 실제로 정수 100인데, 문자열로 형변환하려고 하기 때문입니다.
이런 문제를 줄이기 위해 컬렉션에는 제네릭을 사용하는 것이 좋습니다.
5. 제네릭을 사용한 안전한 코드
이번에는 제네릭을 사용해서 문자열만 저장하도록 만들어보겠습니다.
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("Java");
list.add("Spring");
String first = list.get(0);
String second = list.get(1);
System.out.println(first);
System.out.println(second);
}
}
실행 결과는 다음과 같습니다.
Java
Spring
제네릭으로 String 타입을 지정했기 때문에 값을 꺼낼 때 별도의 형변환을 하지 않아도 됩니다.
6. 제네릭의 장점
제네릭을 사용하면 다음과 같은 장점이 있습니다.
- 잘못된 타입의 값이 들어가는 것을 막을 수 있습니다.
- 값을 꺼낼 때 형변환을 줄일 수 있습니다.
- 코드에서 어떤 타입을 다루는지 명확하게 보입니다.
- 컬렉션을 더 안전하게 사용할 수 있습니다.
- 직접 만든 클래스나 메서드도 여러 타입에 맞게 재사용할 수 있습니다.
특히 컬렉션에서는 제네릭을 거의 기본처럼 사용한다고 생각하면 됩니다.
7. ArrayList와 제네릭
ArrayList<String>은 문자열 목록을 만들 때 사용합니다.
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<String> subjects = new ArrayList<>();
subjects.add("Java");
subjects.add("Spring");
subjects.add("SQL");
for (String subject : subjects) {
System.out.println(subject);
}
}
}
실행 결과는 다음과 같습니다.
Java
Spring
SQL
제네릭 타입이 String이므로 반복문에서도 String subject로 값을 꺼낼 수 있습니다.
8. Integer와 제네릭
정수를 저장하는 ArrayList를 만들 때는 int가 아니라 Integer를 사용합니다.
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<Integer> scores = new ArrayList<>();
scores.add(90);
scores.add(80);
scores.add(70);
int sum = 0;
for (Integer score : scores) {
sum += score;
}
System.out.println("총합: " + sum);
}
}
실행 결과는 다음과 같습니다.
총합: 240
컬렉션에는 기본 자료형을 직접 넣을 수 없기 때문에 int 대신 Integer를 사용합니다.
주의
제네릭 타입에는 기본 자료형을 직접 사용할 수 없습니다. int 대신 Integer, double 대신 Double을 사용합니다.
9. Wrapper 클래스와 제네릭
기본 자료형을 컬렉션 제네릭에 사용할 때는 Wrapper 클래스를 사용합니다.
| 기본 자료형 | 제네릭에서 사용하는 타입 |
int |
Integer |
double |
Double |
boolean |
Boolean |
char |
Character |
long |
Long |
이 부분은 Wrapper 클래스 글과 연결해서 이해하면 좋습니다.
10. HashSet과 제네릭
HashSet에서도 제네릭을 사용해 저장할 타입을 정합니다.
import java.util.HashSet;
public class Main {
public static void main(String[] args) {
HashSet<String> tags = new HashSet<>();
tags.add("Java");
tags.add("Spring");
tags.add("Java");
for (String tag : tags) {
System.out.println(tag);
}
}
}
실행 결과는 다음과 비슷합니다.
Java
Spring
HashSet<String>이므로 문자열만 저장할 수 있고, 중복된 "Java"는 하나만 저장됩니다.
11. HashMap과 제네릭
HashMap은 key와 value 타입을 각각 지정해야 합니다.
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
HashMap<String, Integer> scores = new HashMap<>();
scores.put("Java", 90);
scores.put("Spring", 85);
scores.put("SQL", 80);
System.out.println(scores.get("Java"));
}
}
실행 결과는 다음과 같습니다.
90
HashMap<String, Integer>는 key는 String, value는 Integer라는 뜻입니다.
HashMap<String, Integer>
String → key 타입
Integer → value 타입
12. 직접 만든 객체와 제네릭
제네릭에는 직접 만든 클래스도 사용할 수 있습니다. 예를 들어 상품 객체만 저장하는 ArrayList를 만들 수 있습니다.
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<Product> products = new ArrayList<>();
products.add(new Product("키보드", 50000));
products.add(new Product("마우스", 30000));
for (Product product : products) {
product.printInfo();
}
}
}
class Product {
String name;
int price;
Product(String name, int price) {
this.name = name;
this.price = price;
}
void printInfo() {
System.out.println(name + ": " + price + "원");
}
}
실행 결과는 다음과 같습니다.
키보드: 50000원
마우스: 30000원
ArrayList<Product>는 Product 객체만 저장하는 목록입니다.
13. 다이아몬드 연산자
자바에서는 객체 생성 부분의 제네릭 타입을 생략할 수 있습니다. 이때 사용하는 <>를 다이아몬드 연산자라고 부릅니다.
ArrayList<String> list = new ArrayList<>();
위 코드는 다음과 비슷한 의미입니다.
ArrayList<String> list = new ArrayList<String>();
왼쪽에 ArrayList<String>이라고 타입이 이미 적혀 있으므로, 오른쪽에서는 new ArrayList<>()처럼 간단히 작성할 수 있습니다.
14. raw type이란?
제네릭 타입을 지정하지 않고 컬렉션을 사용하는 것을 raw type이라고 합니다.
ArrayList list = new ArrayList();
이렇게 작성하면 여러 타입의 값이 섞여 들어갈 수 있습니다.
ArrayList list = new ArrayList();
list.add("Java");
list.add(100);
list.add(true);
타입이 섞이면 나중에 값을 꺼낼 때 형변환 오류가 발생할 수 있습니다. 그래서 컬렉션에는 되도록 제네릭 타입을 명확하게 작성하는 것이 좋습니다.
주의ArrayList list = new ArrayList();처럼 제네릭 없이 사용하는 방식은 피하는 것이 좋습니다. 저장할 타입을 명확하게 정해두면 더 안전합니다.
15. 제네릭 클래스 맛보기
제네릭은 컬렉션에서만 사용하는 것이 아닙니다. 직접 만든 클래스에도 사용할 수 있습니다.
아래는 어떤 타입의 값이든 담을 수 있는 간단한 상자 클래스입니다.
class Box<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
여기서 T는 아직 정해지지 않은 타입을 의미합니다. 실제로 사용할 때 타입이 결정됩니다.
16. 제네릭 클래스 사용하기
위에서 만든 Box<T> 클래스를 사용해보겠습니다.
public class Main {
public static void main(String[] args) {
Box<String> stringBox = new Box<>();
stringBox.setValue("Java");
String text = stringBox.getValue();
System.out.println(text);
Box<Integer> intBox = new Box<>();
intBox.setValue(100);
Integer number = intBox.getValue();
System.out.println(number);
}
}
class Box<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
실행 결과는 다음과 같습니다.
Java
100
Box<String>에서는 T가 String처럼 동작하고, Box<Integer>에서는 T가 Integer처럼 동작합니다.
17. 타입 파라미터 이름
제네릭에서 사용하는 T, E, K, V 같은 문자를 타입 파라미터라고 합니다.
| 이름 | 자주 쓰는 의미 | 예시 |
T |
Type | Box<T> |
E |
Element | List<E> |
K |
Key | Map<K, V> |
V |
Value | Map<K, V> |
반드시 이 이름만 써야 하는 것은 아니지만, 자바에서는 관례적으로 이런 문자를 많이 사용합니다.
18. 타입 파라미터가 두 개인 클래스
제네릭 타입은 하나만 사용할 수 있는 것이 아닙니다. 두 개 이상도 사용할 수 있습니다.
class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
}
이 클래스는 key와 value를 한 쌍으로 보관합니다.
public class Main {
public static void main(String[] args) {
Pair<String, Integer> score = new Pair<>("Java", 90);
System.out.println(score.getKey());
System.out.println(score.getValue());
}
}
class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
}
실행 결과는 다음과 같습니다.
Java
90
19. 제네릭 메서드 맛보기
메서드에도 제네릭을 사용할 수 있습니다. 제네릭 메서드는 메서드 호출 시 전달되는 값에 따라 타입이 결정됩니다.
public static <T> void printValue(T value) {
System.out.println(value);
}
<T>는 이 메서드에서 사용할 타입 파라미터를 선언하는 부분입니다.
public class Main {
public static void main(String[] args) {
printValue("Java");
printValue(100);
printValue(true);
}
public static <T> void printValue(T value) {
System.out.println(value);
}
}
실행 결과는 다음과 같습니다.
Java
100
true
문자열, 정수, boolean 값을 모두 같은 메서드로 출력했습니다.
20. 제네릭과 Object의 차이
여러 타입을 다루고 싶다면 Object를 사용할 수도 있습니다. 하지만 Object를 사용하면 값을 꺼낼 때 형변환이 필요할 수 있습니다.
Object 사용
class ObjectBox {
private Object value;
public void setValue(Object value) {
this.value = value;
}
public Object getValue() {
return value;
}
}
값을 꺼낼 때 직접 형변환해야 합니다.
ObjectBox box = new ObjectBox();
box.setValue("Java");
String text = (String) box.getValue();
제네릭 사용
Box<String> box = new Box<>();
box.setValue("Java");
String text = box.getValue();
제네릭을 사용하면 타입이 정해져 있으므로 값을 꺼낼 때 형변환이 필요 없습니다.
21. 제네릭 전체 예제
이번에는 제네릭 컬렉션과 제네릭 클래스를 함께 사용하는 예제를 보겠습니다.
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<Box<String>> boxes = new ArrayList<>();
Box<String> box1 = new Box<>();
box1.setValue("Java");
Box<String> box2 = new Box<>();
box2.setValue("Spring");
boxes.add(box1);
boxes.add(box2);
for (Box<String> box : boxes) {
System.out.println(box.getValue());
}
}
}
class Box<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
실행 결과는 다음과 같습니다.
Java
Spring
ArrayList<Box<String>>는 String 값을 담는 Box 객체들을 저장하는 목록입니다. 처음에는 조금 복잡해 보일 수 있지만, 안쪽부터 읽으면 됩니다.
Box<String> → String을 담는 Box
ArrayList<Box<String>> → String Box들을 담는 ArrayList
22. 제네릭 사용 시 자주 하는 실수
제네릭을 처음 배울 때는 아래와 같은 실수를 자주 합니다.
| 실수 | 문제점 | 해결 방법 |
| 제네릭 타입을 작성하지 않음 | 여러 타입이 섞여 들어갈 수 있음 | ArrayList<String>처럼 타입 지정 |
ArrayList<int> 사용 |
기본 자료형은 제네릭에 직접 사용 불가 | ArrayList<Integer> 사용 |
| HashMap의 key와 value 타입 순서 혼동 | 저장 타입을 잘못 이해할 수 있음 | HashMap<K, V>로 기억 |
| 제네릭과 Object를 같은 것으로 생각 | 형변환과 타입 안정성 차이를 놓침 | 제네릭은 타입을 미리 정해 안전하게 사용 |
| 너무 복잡한 제네릭을 한 번에 이해하려 함 | 코드가 어렵게 느껴짐 | 안쪽 타입부터 차례대로 읽기 |
23. 직접 연습해보기
아래 코드를 직접 작성하고 실행해보세요.
import java.util.ArrayList;
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
ArrayList<Product> products = new ArrayList<>();
products.add(new Product("키보드", 50000));
products.add(new Product("마우스", 30000));
for (Product product : products) {
product.printInfo();
}
HashMap<String, Product> productMap = new HashMap<>();
productMap.put("P001", new Product("모니터", 200000));
productMap.put("P002", new Product("스피커", 70000));
Product foundProduct = productMap.get("P001");
foundProduct.printInfo();
Box<Product> productBox = new Box<>();
productBox.setValue(new Product("노트북", 1200000));
productBox.getValue().printInfo();
}
}
class Product {
String name;
int price;
Product(String name, int price) {
this.name = name;
this.price = price;
}
void printInfo() {
System.out.println(name + ": " + price + "원");
}
}
class Box<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
실행 결과는 다음과 같습니다.
키보드: 50000원
마우스: 30000원
모니터: 200000원
노트북: 1200000원
24. 이번 글 정리
이번 글에서는 타입을 미리 지정해서 안전하게 코드를 작성할 수 있게 해주는 제네릭에 대해 정리했습니다. 핵심 내용은 다음과 같습니다.
- 제네릭은 클래스나 메서드에서 사용할 타입을 미리 정해두는 문법이다.
- 제네릭은 꺾쇠괄호
< >안에 타입을 작성한다. ArrayList<String>은 String만 저장하는 ArrayList이다.HashMap<String, Integer>는 String key와 Integer value를 저장하는 HashMap이다.- 제네릭을 사용하면 잘못된 타입의 값이 들어가는 것을 막을 수 있다.
- 제네릭을 사용하면 값을 꺼낼 때 형변환을 줄일 수 있다.
- 컬렉션 제네릭에는 기본 자료형을 직접 사용할 수 없다.
- 기본 자료형 대신
Integer,Double,Boolean같은 Wrapper 클래스를 사용한다. - 직접 만든 클래스도 제네릭 타입으로 사용할 수 있다.
new ArrayList<>()의<>는 다이아몬드 연산자라고 부른다.- 제네릭 타입을 지정하지 않는 raw type 사용은 피하는 것이 좋다.
- 직접 제네릭 클래스나 제네릭 메서드를 만들 수도 있다.
한 줄 요약
제네릭은 사용할 데이터 타입을 미리 정해두어 잘못된 타입 사용을 막고, 형변환 없이 안전하게 값을 다룰 수 있게 해주는 자바 문법입니다.
다음 글 예고
다음 글에서는 [Java 개념노트 53] 제네릭 클래스와 제네릭 메서드 이해하기라는 주제로 직접 타입 파라미터를 가진 클래스를 만들고 메서드에서 제네릭을 사용하는 방법을 더 자세히 정리해보겠습니다.
GWDEVELBlog Java 개념노트 시리즈

'Computer Science > Java' 카테고리의 다른 글
| [Java ] 개념노트 54 제한된 제네릭과 와일드카드 이해하기 (0) | 2026.06.18 |
|---|---|
| [Java ] 개념노트 53 제네릭 클래스와 제네릭 메서드 이해하기 (0) | 2026.06.17 |
| [Java] 개념노트 51 Iterator와 컬렉션 반복 이해하기 (0) | 2026.06.17 |
| [Java ] 개념노트 50 Map과 HashMap 이해하기 (0) | 2026.06.17 |
| [Java] 개념노트 49 Set과 HashSet 이해하기 (0) | 2026.06.16 |