[이펙티브 자바] Generic 으로 만들어 사용하기
by rowing0328※ 책 내용을 바탕으로 제 관점에서 풀어쓴 글입니다. 일부 내용이 다를 수 있습니다.
아이템 26 - 로 타입은 사용하지 말라
Raw 타입이란
제네릭이 도입되기 이전의 컬렉션 클래스와 같이 타입 매개변수를 지정하지 않은 상태의 타입을 의미한다.
예제 코드
List test = new ArrayList<>();
test.add("String");
test.add(1); // 서로 다른 타입의 객체 추가 허용
설명
위 코드는 컴파일러가 타입 안전성을 보장하지 못하기 때문에
런타임 에러가 발생할 가능성이 크다.
예를 들어, String과 Integer 같은 서로 다른 타입의 데이터를
하나의 리스트에 추가해도 컴파일 에러는 발생하지 않는다.
하지만, 해당 리스트를 사용하는 코드에서
예상치 못한 문제가 발생할 수 있다.
Raw 타입 사용의 문제
- 컴파일 시점
컴파일러가 타입 검사(Type Check)를 하지 않으므로 잘못된 데이터 삽입 가능하다. - 런타임 시점
프로그램이 실행되는 동안 타입 불일치로 인해 ClassCastException 같은 에러가 발생할 위험이 있다.
Unbounded Wildcard Type
Raw 타입 대신 Unbounded Wildcard Type(List<?>)을 사용할 수 있다.
이 타입은 읽기 전용으로 적합하며, 쓰기 작업은 제한적이다.
예제 코드
private int add(List<?> list) {
list.add(1); // 컴파일 에러 : Unbounded Wildcard는 읽기 전용으로 사용
return 1;
}
설명
List<?>는 타입 안전성을 보장하지만, 내부에 값을 추가하는 등의 쓰기 작업에는 적합하지 않다.
왜 Unbounded Wildcard를 사용할까
- 타입 안전성
Raw 타입과 달리 List<?>는 컴파일러가 타입 검사를 수행한다. - 읽기 전용
리스트의 데이터를 읽어오는 작업에서는 유용하다.
명확한 타입 추론의 필요성
제네릭을 사용하지 않으면 타입 추론이 불가능해지고, 결과적으로 런타임 에러가 발생할 수 있다.
잘못된 예제 코드
static List listMerge(List a, List b) {
List c = new ArrayList();
c.addAll(a); // 타입 정보가 없어 문제 발생 가능
c.addAll(b);
return c;
}
설명
위 코드는 타입 정보를 명시하지 않아 런타임 에러가 발생할 가능성이 크다.
List의 타입 매개변수를 지정하지 않으면 컴파일러가 타입 충돌을 탐지할 수 없다.
올바른 예제 코드
static <T> List<T> listMerge(List<T> a, List<T> b) {
List<T> c = new ArrayList<>();
c.addAll(a);
c.addAll(b);
return c;
}
설명
제네릭을 사용하여 타입을 명시하면 컴파일러가 타입 충돌을 탐지할 수 있어 더 안전한다.
예외적인 Raw 타입 사용
모든 경우에 Raw 타입이 금지되는 것은 아니다.
일부 상황에서는 Raw 타입이 필요할 수 있다.
예제 코드
# Class 리터럴
List.class;
# 런타임 시 제네릭 타입 소거(Erasure) 처리
if (a instanceof Set) {
Set<?> set = (Set<?>) a;
for (Object o : set) {
if (o instanceof String) {
// 처리
}
}
}
설명
이 경우, Raw 타입을 사용하지 않고는 제네릭 정보를 소거한 객체를 처리하기 어렵다.
Generic Class / Interface
Java의 컬렉션 클래스는 제네릭을 사용하도록 설계되어 있다.
제네릭을 사용하면 타입 안정성을 높이고 재사용성을 극대화할 수 있다.
사용 예시
// 제네릭을 사용한 클래스 선언
List<String> test = new ArrayList<>();
// Generic Interface
public interface List<E> extends Collection<E> { ... }
// Generic Class
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { ... }
아이템 27 - 비검사 경고를 제거하라
컴파일러 경고란
코드에 잠재적인 문제가 있음을 알려주는 메시지이다.
예를 들어, 제네릭 타입을 정확히 명시하지 않았을 때 경고가 발생한다.
예제 코드
Set<String> test = new HashSet(); // 경고 발생
설명
- 경고 메시지
Raw use of parameterized class ‘HashSet’
Unchecked assignment: ‘java.util.HashSet’ to ‘java.util.Set<java.lang.String>’
이는 HashSet의 타입 파라미터가 명확히 지정되지 않았기 때문에 발생하는 경고입니다.
경고 해결 방법
컴파일러 경고를 제거하려면 아래 방법을 사용할 수 있다.
예제 코드
Set<String> test = new HashSet<>(); // 경고 제거
@SuppressWarnings("unchecked")
Set<String> test = new HashSet();
설명
- 제네릭 타입 명시
제네릭 타입을 명확히 지정하여 컴파일러가 타입 안정성을 확인할 수 있도록 한다. - 경고가 안전하다고 확신할 경우
만약 코드가 안전하다고 확신할 수 있다면, @SuppressWarnings("unchecked") 어노테이션을 사용해 경고를 숨길 수 있다.
주의사항
@SuppressWarnings를 사용하기 전에, 해당 코드가 정말로 타입 안정성을 보장하는지 충분히 검토해야 한다.
경고를 무시하면 발생할 수 있는 문제
- 치명적인 에러를 놓칠 가능성
컴파일러 경고는 단순한 알림 이상의 역할을 한다.
경고를 무시하면 치명적인 문제를 감지하지 못할 수 있다. - 잘못 작성된 코드를 지나칠 가능성
경고가 발생한 코드를 무시하다 보면, 코드 내의 논리적 오류나 설계 문제를 지나칠 수 있다. - 중요한 경고를 놓칠 위험
경고가 너무 많으면, 정말로 중요한 경고를 인지하지 못할 가능성이 크다.
특히, 외부 라이브러리와의 의존성을 추가할 때 이런 상황이 자주 발생한다. - 협업 시 신뢰 저하
동료 개발자가 경고를 무시한 코드를 보면, 해당 코드의 안전성을 의심하거나 코드 리뷰 과정에서 추가적인 불편함을 겪게 될 수 있다.
아이템 28 - 배열보다는 리스트를 사용하라
배열과 제네릭은 서로 다른 타입 안전성 규칙을 따르기 때문에, 배열보다 제네릭을 사용하는 것이 더 안전한다.
예제 코드
// 런타임 에러 발생
Object[] objects = new Long[1];
objects[0] = "test";
// 컴파일 에러 발생
List<Object> objectList = new ArrayList<Long>();
설명
- 배열 사용 시 발생할 수 있는 문제
배열은 타입 불일치가 발생하면 컴파일 단계에서는 확인할 수 없고, 런타임에서만 에러가 발생한다. - 제네릭 사용 시 컴파일 에러
제네릭은 타입 안전성을 컴파일 단계에서 보장하므로, 이런 불일치를 사전에 방지할 수 있다.
배열과 리스트의 차이
- 배열
배열은 공변이라서 잘못된 타입으로 업캐스팅(예: String[] → Object[])이 가능하다.
하지만 이런 특성 때문에 런타임에 타입 오류(ArrayStoreException)가 발생할 위험이 있다. - 제네릭 (List)
리스트는 불공변이라 잘못된 타입으로 업캐스팅이 허용되지 않는다.
덕분에 컴파일 시점에 타입 안정성이 보장되고, 런타임 오류를 방지할 수 있다.
따라서, 리스트(List)를 사용하는 것이 바람직하다.
아이템 29 - 이왕이면 제네릭 타입으로 만들라
Type Parameter Naming Conventions
- E (Element)
요소를 나타낼 때 사용한다. - K (Key)
키를 나타낼 때 사용한다. - N (Number)
숫자를 나타낼 때 사용한다. - T (Type)
일반 타입을 나타낼 때 사용한다. - V (Value)
값을 나타낼 때 사용한다. - S, U, V
2번째, 3번째, 4번째 타입을 나타낼 때 사용한다.
예제 코드
@NoRepositoryBean
public interface JpaRepository<T, ID> { }
설명
- T
엔티티 타입 - ID
엔티티의 고유 식별자 타입
Generic Type Example
제네릭 타입의 장점을 이해하기 위해 간단한 덧셈 및 뺄셈 계산기를 구현해 보았다.
이 계산기는 제네릭 타입 E를 사용하여 다양한 데이터 타입을 유연하게 처리할 수 있다.
예제 코드
public class Calculator<E> {
private StringBuilder expression;
public Calculator() {
expression = new StringBuilder();
}
public void add(E e) {
expression.append("+" + e.toString());
}
public void minus(E e) {
expression.append("-" + e.toString());
}
public String expression() {
if (expression.charAt(0) == '+') {
return expression.substring(1); // '+' 제거
} else {
return expression.toString();
}
}
}
설명
- 제네릭 타입 E
타입에 따라 다양한 데이터(Integer, Double 등)를 처리할 수 있다.
강력한 타입 안정성과 재사용성 제공한다. - 동적 표현식 생성
add()와 minus() 메서드를 호출하여 수식 표현식을 동적으로 생성한다.
타입에 관계없이 표현식을 생성 가능하다.
왜 제네릭 타입을 사용해야 하는가
- 타입 안정성 보장
Object를 사용하는 경우, 명시적 형변환이 필요해 런타임 에러 가능성이 있다.
제네릭을 사용하면 컴파일 시 타입 검증이 이루어져, 타입 안전성을 확보할 수 있다. - 가독성과 유지보수성 개선
명시적 형변환이 없어 코드가 깔끔해지고, 가독성이 향상된다.
타입에 대한 제약 조건을 명시적으로 정의할 수 있어, 유지보수성과 확장성이 높아진다.
마무리
이번 아이템을 통해 제네릭 타입의 활용과 장점,
그리고 Java의 타입 안전성을 보장하기 위한 설계 원리를 이해할 수 있었다.
특히, 제네릭 타입 소거라는 개념이 흥미로웠다.
컴파일 시점에서 타입 검증을 통해 런타임 에러 가능성을 최소화하고,
기존 코드와의 호환성을 유지하는 설계는 Java가 가진 큰 강점 중 하나였다.
이러한 구조 덕분에 제네릭을 활용하면 타입 안정성을 확보하면서도
더 유연하고 재사용 가능한 코드를 작성할 수 있음을 배울 수 있었다.
'📚Book Archive > Effective Java' 카테고리의 다른 글
[이펙티브 자바] Generic method와 Generic의 주의사항 (0) | 2025.01.08 |
---|---|
[이펙티브 자바] Interface와 Class 설계 원칙 (0) | 2024.12.30 |
[이펙티브 자바] Class와 상속 (4) | 2024.12.20 |
[이펙티브 자바] 모든 객체의 공통 메서드 (0) | 2024.12.13 |
[이펙티브 자바] 객체 파괴 (0) | 2024.12.07 |
블로그의 정보
코드의 여백
rowing0328