최근 JDBC를 이용해 정말 간단한 이커머스 CRUD를 개발하면서 팀원들과 제네릭 사용에 대해 많이 고민했다. 제네릭을 따로 정의해서 사용해본 경험이 거의 없었기 때문에 정확히 왜 사용하고 어떤 점에서 타입 안정성이 보장된다고 하는지 알고 싶어 Effective Java에 제네릭 부분을 읽고 정리하게 되었다.
Generic이란?
Java 5 부터 사용할 수 있는 generic은 컬렉션이 담을 수 있는 타입을 컴파일러에 알려준다. 꺽쇠 괄호에 클래스 타입을 명시하는 형태로 사용하며 타입 안정성을 보장하고 확장성을 높여준다.
raw 타입은 사용하지 마라
Raw type은 List<E>
에서 보면 List
가 raw type이다. Raw type은 타입 선언에서 제네릭 타입 정보가 전부 지워진 것처럼 동작하게 된다. Raw type을 사용하면 개발자가 실수로 다른 타입을 넣어도 컴파일 시점에서는 알 수 없게 된다. 개발 시에는 컴파일 시점에 오류를 알 수 있는 것이 가장 좋다.
1
2
3
4
public class GenericPractice {
List raw = new ArrayList(); // Raw use of parameterized class 'ArrayList'
List<String> generic = new ArrayList<>();
}
Raw 타입을 사용하게 되면 intellij에서 raw 타입 사용이라는 경고를 알려준다. 제네릭을 사용하여 List가 어떤 타입으로 구성되어야 하는지 정의해준다.
제네릭 타입, 메서드로 만들어 사용하라
1
2
3
4
5
6
7
8
9
public class GenericStack {
public static void main(String[] args) {
Stack<String> stack = new Stack<>();
for (String arg :
args) {
stack.push(arg);
}
}
}
제네릭 스택을 사용한 코드이다. String 타입을 가지는 스택으로 정의한 것처럼, 타입 매개변수에 다양한 타입을 사용할 수 있다. Object, int[], List
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BaseEntity<ID extends Number> {
ID userId;
public BaseEntity(ID userId) {
this.userId = userId;
}
public static void main(String[] args) {
long a = 2L;
BaseEntity<Long> baseEntity = new BaseEntity<>(a);
System.out.println(baseEntity.userId);
}
}
위와 같이 BaseEntity의 ID를 정의하는데 Number 타입의 ID를 사용해야 한다. Number 타입에는 Integer, Long, Double 등을 사용할 수 있다.
1
BaseEntity baseEntity = new BaseEntity(a);
위와 같이 BaseEntity를 정의하게 되면 경고가 뜨게 되며 raw 타입을 제네릭하게 변경하도록 제안을 한다. BaseEntity
클래스 설계 시 형변환 없이 사용할 수 있도록 제네릭을 도입하는 것이 타입 안정성을 높일 수 있는 방법이다.
재귀적 타입 한정을 이용해 상호 비교를 표현한다
1
public static <E extends Comparable<E>> E max(Collection<E> c)
위 코드는 모든 E 타입은 자신과 비교할 수 있다라고 이해할 수 있다. 제네릭 메서드를 사용하면 입력 매개변수와 반환값을 명시적으로 형변환해야 하는 메서드보다 안전하고 사용하기 쉽다.
타입과 메서드는 형변환 없이 사용할 수 있는 것이 좋으며, 제네릭을 사용하면 이것을 해결할 수 있다.