제네릭이란 무엇이며, 왜 사용할까요?
또한 제네릭을 사용하여 어떤 이점을 얻을 수 있나요?
제네릭은 클래스, 인터페이스 및 메서드를 정의할 때, 실제 사용될 타입을 타입 파라미터로 받아 사용하는 방식이에요. 관례적으로 타입파라미터는 Type의 T와 Element의 E 등을 사용해요.
public class ResponseEntity<T> extends HttpEntity<T>
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
타입 파라미터는 제네릭에서 사용되는 변수처럼 동작하는 것으로 클래스(인터페이스)가 생성될 때, 메서드가 호출될 때 실제 타입으로 대체되요.
제네릭은 컴파일시에 타입을 확인하기에 잘못된 타입으로 인한 런타임 오류를 방지해요. 즉, 컴파일 시점에서 타입의 오류를 알기 때문에 개발자가 쉽게 확인할 수 있어요.
제네릭을 사용하면 다양한 타입에 대해 동일한 클래스나 메서드를 재사용할 수 있어요. 타입에 따라 새로운 클래스를 정의할 필요 없이 제네릭을 활용하여 타입 파라미터에 각각의 타입을 넣어 사용할 수 있어요.
이는 오버로딩과 비슷한 느낌이에요. 파라미터의 개수나 타입에 따라 같은 메서드명을 사용할 수 있는 것처럼 같은 클래스(인터페이스)명이지만 타입에 따라 새로 만드는 것이 아니라 안에 알맞은 타입을 넣고 사용할 수 있어요.
타입 파라미터를 사용하여 코드의 의도를 명확히 파악할 수 있고, 굳이 형변환을 하지 않아도 돼요.
제네릭을 사용하면 컬렉션 클래스를 타입 안정성 있게 사용하도록 도와주어요. 컴파일러가 타입 체크를 수행하여 잘못된 타입의 요소가 컬렉션에 추가되는 것을 방지할 수 있어요.
제네릭을 사용할 때 <>안에 들어가는 타입은 어떤 타입이라도 상관이 없어요.
메소드의 매개 변수로 넘어갈 때 제네릭에 사용 되는 ‘?’를 와일드 카드 타입이라고 해요.
와일드 카드로 선언한 후에 객체의 값을 가져올 수는 있지만, 특정 타입으로 값을 지정하는 것은 불가능해요.
WildCardGeneric
public class WildCardGeneric<W> {
W wildCard;
public void setWildCard(W wildCard) {
this.wildCard = wildCard;
}
public W getWildCard() {
return wildCard;
}
}
WildCardMain
public class WildCardMain {
public static void main(String[] args) {
WildCardMain wildCardMain = new WildCardMain();
wildCardMain.callWildCardGeneric();
}
public void callWildCardGeneric() {
WildCardGeneric<?> wildCardGeneric = new WildCardGeneric<>();
wildCardGeneric.setWildCard("hello");
wildCardStringMethod(wildCardGeneric);
}
// 메소드에서 제네릭 와일드카드 사용
public void wildCardStringMethod(WildCardGeneric<?> c) {
Object value = c.getWildCard();
System.out.println(value);
}
}
와일드카드는 메소드의 매개 변수로 사용하는 것이 좋아요.
위와 같이 설계했을 때 다음과 같은 컴파일 오류를 찾을 수 있어요.
제네릭을 사용할 때 <> 안에는 어떠한 타입이라도 상관 없다고 했지만 와일드카드 처럼 타입을 애매하게 하는 것보다는 명시적으로 타입을 지정해 주면 코드의 예측가능성을 올릴 수 있어요.
범위를 제한하는 방식은 두 가지 방식이 있어요.
해당 와일드 카드는 상위타입이거나 상위타입의 하위타입으로 제한하는 방식이에요.
데이터를 리턴하는 메소드(Producer)에서는 ? extends T
를 사용해요.
읽기 작업(상위 타입의 요소를 가져오는 작업)에 사용돼요.
해당 와일드 카드는 하위타입 이거나 하위타입의 상위타입으로 제한하는 방식이에요.
쓰기 작업(하위타입으로 요소를 추가하는 작업)에 사용돼요.
제네릭에서 와일드카드 타입을 사용할 때 권장되는 설계 원칙이에요. 와일드카드 타입이 매개변수로 사용되는 경우 데이터를 생산(리턴)하는 메소드에서는 extends
를 사용하고, 데이터를 소비(파라미터로 받음)하는 메소드에서는 super
를 사용하는 것을 의미해요.
PECS 원칙을 적용하면, 제네릭 타입의 유연성을 높일 수 있고, 타입 안정성을 유지하면서 다양한 타입의 데이터를 처리할 수 있게 돼요. 이를 통해 제네릭 코드의 재사용성과 확장성을 향상시킬 수 있어요.
소프트웨어 설계 원칙 중 하나로, 한 메서드는 command와 query를 동시에 처리하지 말아야 한다는 원칙이에요.
CQS를 따르는 코드에서는 명령과 질의를 수행하는 메서드가 분리되어야 해요.
명령을 처리하는 메서드는 어떤 작업을 수행하고 객체 상태를 변경하며, 반환값이 없어야 해요.
반면, 질의를 처리하는 메서드는 객체의 상태를 변경하지 않고 필요한 정보를 반환해야 해요. 이렇게 분리된 메서드는 코드를 이해하기 쉽게 만들고, 응집력 있는 클래스를 설계를 할 수 있어요.
CQS를 따르지 않으면 다음과 같은 문제를 야기해요.
CQS을 지키면 코드의 의도를 명확하게 드러내며(코드 가독성 증가), 부작용(side effect)이 최소화되고 예측 가능한 동작을 가진 코드를 작성할 수 있게 되요. 이를 통해 코드의 가독성과 유지보수성이 향상되며, 버그 발생 가능성을 줄일 수 있어요.