본 포스팅에서 제네릭이란 무엇인지, 왜 필요한지에 대해 다루며, 이를 활용하는 기초적인 방법에 대해서 다루겠습니다.
제네릭의 고급 내용인 와일드 카드 및, Upper Bound, LoweBound에 대한 설명은 다음 포스팅을 참조해주시기 바랍니다 :)
Generic 이란?
제네릭이란 타입의 일반화를 말합니다.
" 타입의 일반화 " 란 어떤 클래스를 작성할때, 클래스 내부에서 타입을 지정하는것이 아닌, 클래스를 사용하는 외부에서 타입을 지정할 수 있게끔 한다는 것입니다.
" 외부에서의 지정 " 란 사용자가 정의된 클래스를 사용할때, 즉 인스턴스를 생성하거나 클래스의 제네릭 메소드를 사용 할때 타입을 지정한다는 의미입니다.
이번 포스팅에서는 한가지 질문과 함께 제네릭의 필요성과 이점에 대해 설명드리겠습니다.
문제 제기
어떤 자료구조를 구현한다고 하겠습니다. 자료구조는 어떤 데이터 타입(Integer, Long, String, ...)이든 동일한 구조를 구성하여 자료구조만의 작업을 수행할 수 있어야 합니다. 이를 위해서 데이터 타입마다 자료구조를 구현해야 할까요?
여러 타입을 지원하기 위한 방안 ( + 제네릭 등장배경 )
이를 위해 자바의 Object 클래스를 사용하는 방법과 제네릭을 사용하는 방법이 있습니다.
Object는 사실 자바5 이전에 사용되던 간단한 해결안 이였습니다. 자바의 최상위 객체 Object를 이용하여 어떤 데이터 타입이든 지원할 수 있습니다. 하지만, 이런 간단한 해결안이 있음에도, 왜 Java5에 제네릭이 등장했고, 이를 통상적으로 이용할까요? 제네릭의 필요성을 이해하기 위해 먼저 Object를 통해 간단히 Bag객체를 구현해보겠습니다.
Object를 이용한 Bag class 입니다. Bag 클래스는 임의의 객체를 저장하고, 다시 가져올 수 있는 기능을 하도록 구현했습니다.
class Bag {
private Object stuff;
// 오브젝트 타입으로 필드를 선언함으로써
// 어떤 타입의 객체든지 저장할 수 있도록 합니다.
public Object getStuff() {
return stuff;
}
public void setStuff(Object stuff) {
this.stuff = stuff;
}
}
이를 통해 자바의 어떤 객체든 이 Bag 클래스 내에 저장 할 수 있습니다.
public class Main {
public static void main(String[] args) {
String stuff = "pencil";
Bag bag = new Bag();
bag.setStuff(stuff); // 가방에 물건을 담습니다.
// String resultStuff = bag.getStuff(); 1. getStuff() 함수는 리턴 타입이 Object이기 때문에 String 타입으로의 명시적인 캐스팅이 필요합니다.
// long resultStuff = (Long) bag.getStuff(); 2. Object를 잘못 캐스팅해버려 런타임 내에 에러가 발생할 수 있습니다.
String resultStuff = (String) bag.getStuff();
}
}
하지만, Bag클래스가 Object 타입을 사용함에 따라 해당 클래스의 메소드는 입력 타입, 리턴타입이 모두 Object 타입 입니다.
따라서 이를 사용하는 외부 클래스들은 메소드가 어떤 타입을 리턴 하는지를 항상 인지하고 있어야 하며, 이를 명시적으로 캐스팅해야 한다는 단점이 있었습니다.
또한, 위의 주석처럼 컴파일 타임에서 에러를 감지할 수 없으므로, 잘못된 캐스팅으로 인해 런타임 에러가 발생할 수 있습니다. 이는 어플리케이션이 배포되어 실행되는 중에 강제종료되는 등 치명적인 상황을 초래 했었습니다.
제네릭은 이와같은 타입의 안전성 문제를 해결하기 위해 등장했습니다.
이제, 제네릭을 이용해 Bag 클래스를 구현 해보겠습니다.
class Bag<E> {
private E stuff;
public E getStuff() {
return stuff;
}
public void setStuff(E stuff) {
this.stuff = stuff;
}
}
제네릭을 이용해 인스턴스를 생성했을때, 제네릭 클래스의 메소드들은 일관적인 데이터 타입을 가질 수 있게 됩니다. 컴파일 단계에서
제네릭으로 표기된 클래스들의 메소드, 필드들의 데이터 타입을 입력된 데이터 타입으로 전부 캐스팅 해주기 때문에, 사용자는 명시적으로 캐스팅을 할 필요도 없을뿐더러 잘못된 데이터 타입 사용 시 컴파일 타임에 이를 확인할 수 있습니다.
모든 제네릭 타입은 컴파일 타임에 명시된 클래스 타입으로 변환됨으로, 실행 중 더이상 캐스팅이 발생하지 않게 됩니다.
public class Main {
public static void main(String[] args) {
String stuff = "pencil";
Bag<String> bag = new Bag<>();
bag.setStuff(stuff); // 가방에 물건을 담습니다.
String resultStuff = bag.getStuff(); // 가방에서 물건을 가져옵니다.
}
}
이렇게 제네릭을 이용함으로써 클래스가 사용할 데이터타입을 외부에서 지정하여 사용할 수 있으며, 더이상 캐스팅을 위해 데이터 타입에 신경 쓰지 않아도 됩니다.
제네릭의 이점
따라서, 제네릭을 사용함으로써 다음과 같은 이점이 있습니다.
- 재사용성
제네릭을 사용하는 클래스는 여러 타입을 지원할 수 있습니다. - 컴파일 시 타입 에러 발견
런타임 내에서 발생 할 수 있던 형변환 에러를 미리 컴파일타임에서 감지할 수 있게 됩니다. - 명시적 캐스팅 생략
개발자가 명시적으로 캐스팅할 필요가 없어졌습니다.
이제 제네릭을 어떻게 사용할 수 있는지 살펴보겠습니다.
제네릭 클래스 사용방법
제네릭 타입이란, 타입을 파라미터로 가지는 클래스혹은 인터페이스를 말합니다.
타입은 <>을 클래스 혹은 인터페이스이름 옆에 붙여 사용할 수 있으며, 아래와 같이 사용가능합니다. 또한 여러 타입을 지정 받도록 구현할 수 있습니다.
interface 인터페이스<T> {}
class 클래스명<E> {}
class 클래스명<K, V> {}
interface 인터페이스<K, V> {}
위처럼 타입을 명시하면, 해당 인터페이스를 구현하거나 클래스를 구현할때 제네릭 타입을 인스턴스 변수, 인스턴스 메소드 등에 명시하여 지정된 타입을 사용하도록 할 수 있습니다.
class Bag<E> {
private E stuff; // E 타입 인스턴스 변수를 가지도록
public E getStuff() { // E 타입을 리턴하도록
return stuff;
}
public void setStuff(E stuff) { // E 타입을 파라미터로 받도록
this.stuff = stuff;
}
}
제네릭 메소드 사용방법
인스턴스가 생성될때 같이 초기화되는 인스턴스 변수, 메소드의 경우에는 위처럼 인스턴스 생성시 타입을 지정할 수 있습니다.
하지만 아시다시피, 자바에는 static 키워드를 사용한 정적 변수, 정적메소드가 존재합니다. 이들은 인스턴스 생성 시가 아닌, 클래스 로더에 의해 클래스가 처음 메모리에 적재될 때 초기화 됩니다.
따라서, 제네릭 메소드는 인스턴스 생성시 지정한 타입과는 독립적인 타입을 사용합니다.
public class Main {
public static void main(String[] args) {
Bag<Integer> bag = new Bag<>(); // bag 인스턴스를 Integer 타입으로 지정
bag.setStuff(12);
System.out.println(bag.getStuff());
Bag.genericMethod("Hello"); // Integer과 독립적으로 String 타입 사용
}
}
class Bag<E> {
private E stuff; // E 타입 인스턴스 변수를 가지도록
public E getStuff() { // E 타입을 리턴하도록
return stuff;
}
public void setStuff(E stuff) { // E 타입을 파라미터로 받도록
this.stuff = stuff;
}
public static <E> void genericMethod(E stuff) { // E 라는 이름은 같지만, 인스턴스와 독립적으로 실행
System.out.println("hello");
}
}
이렇게 간단히 자바의 제네릭의 기초에 대해 알아봤습니다.
다음 포스팅에서는 이번 포스팅에서 다룬 제네릭에 대한 이해를 바탕으로 제네릭에 대해 깊이 탐구해 보겠습니다.
긴 글 읽어주셔서 감사합니다.
'Java' 카테고리의 다른 글
Java IO / NIO (0) | 2024.12.16 |
---|---|
[Java] Optional 클래스 살펴보기 (2) (0) | 2024.01.24 |
[Java] Optional 클래스의 등장 (1) (2) | 2024.01.23 |
[Java] 제네릭 - 와일드 카드 (0) | 2024.01.15 |
[Java] Collection Framework (1) | 2024.01.10 |