본문 바로가기

Java

제네릭이란? 제네릭(generic) 예제

제네릭(Generic)

사전적 의미 : 포괄적인, 회사 이름이 붙지않은

하지만 프로그래밍적 관점에서 보자면 비슷하지만 다른 의미로 사용된다.

클래스 내부에서 사용할 데이터 타입을 외부에서 지정하여 사용하는 방법, 즉 클래스 내부 데이터 타입을 인스턴스 생성할 때 확정하는 것을 제네릭(Generic)이라고 한다.


우선 제네릭이 무엇인지 알아보자.

package devhong.tistory.com.generic;

class Person<T>{
	public T info;
	Person(T info){
		this.info = info;
	}
}

public class GenericMain {
	public static void main(String[] args) {
		Person<String> p1 = new Person<String>("test");
		Person<Integer> p2 = new Person<Integer>(3);
		
		System.out.println(p1.info);
		System.out.println(p2.info);
	
	}
}

위 소스를 보면 Person 클래스 매개변수가 T이며, main메소드에서 인스턴스 타입을 지정해서 생성하는 것을 볼 수 있는데. p1와 p2 생성에 쓰인 클래스는 Person으로 동일하지만, 각각의 타입은 String과 Integer로 다른 것을 확인할 수있다.



그렇다면 제네릭이 필요한 이유를 알아보자.


아래 소스는 제네릭을 사용하지 않은 코드이다.

package devhong.tistory.com.generic;

class StudentInfo{
    public int grade;
    StudentInfo(int grade){ this.grade = grade; }
}
class StudentPerson{
    public StudentInfo info;
    StudentPerson(StudentInfo info){ this.info = info; }
}
class ProfessorInfo{
    public int rank;
    ProfessorInfo(int rank){ this.rank = rank; }
}
class ProfessorPerson{
    public ProfessorInfo info;
    ProfessorPerson(ProfessorInfo info){ this.info = info; }
}

public class GenericMain {
	public static void main(String[] args) {
		StudentInfo si = new StudentInfo(2);
		StudentPerson sp = new StudentPerson(si);
		ProfessorInfo pi = new ProfessorInfo(3);
		ProfessorPerson pp = new ProfessorPerson(pi);
		
		System.out.println(sp.info.grade);
		System.out.println(pp.info.rank);
	}
}

코드를 보면 StudentPerson과 ProfessorPerson, 그리고 StudentInfo와 ProfessorInfo가 같은 구조로 되어있는 것을 알 수 있다.


코드의 재사용성을 위해 다음과 같이 간략화 해보았다.

package devhong.tistory.com.generic;

class Info{
    public int data;
    Info(int data){ this.data = data; }
}
class Person{
    public Info info;
    Person(Object info){ this.info = (Info)info; }
}

public class GenericMain {
	public static void main(String[] args) {

		Info si = new Info(5);
		Person sp = new Person(si);
		
		System.out.println(sp.info.data);
		
	}
}

이와 같이 변경했을 때 중복소스도 제거했고 컴파일, 실행도 정상적으로 이루어지므로 매우 합리적인 코딩으로 보인다.


하지만 Person의 생성자에 Object로 타입을 정하였기 때문에 이러한 문제점이 생길 수 있다.


public class GenericMain {
	public static void main(String[] args) {

		Info si = new Info(5);
		Person sp = new Person(si);
		
		Person p1 = new Person("테스트");
		
		Info pi = (Info)sp.info;
		System.out.println(pi.data);
		
		System.out.println(sp.info.data);
		
	}
}


Person클래스 매개변수에 "테스트"라는 String을 넘기게 되면? 소스 상 에러는 없다고 뜬다.

하지만 런타임 에러가 발생한다.




컴파일 기반의 언어는 다음과 같은 원칙을 가지고 있다..

모든 에러는 컴파일에서 발생해야하며, 만약 컴파일에서 에러가 발생하지 않고 실제 동작하는 애플리케이션에서 런타임에러가 발생할 경우 대부분 심각한 상황을 초래하기 때문이다.


위와 같은 에러를 타입이 안전하지 않다고 한다. 즉 모든 타입이 올 수 있기 때문에 타입을 엄격하게 정의 할 수 없게 되는 것이다.


이를 해결하기 위해 제네릭(Generic)이라는게 도입되었다.


위 코드를 제네릭으로 바꿔보자.


package devhong.tistory.com.generic;

class Info{
    public int data;
    Info(int data){ this.data = data; }
}
class Person<T>{
    public T info;
    Person(T info){ this.info = info; }
}

public class GenericMain {
	public static void main(String[] args) {

		Info si = new Info(5);
		Person<Info> p1 = new Person<Info>(si);
		System.out.println(p1.info.data);	//성공
		
		Person<String> p2 = new Person<String>("테스트"); 
		System.out.println(p2.info.data);	//에러
	
	}
}

이와 같이 변형하였을 때 에러는 p2.info.data에서만 발생하는데 p2.info는 String이고 String에서 data라는 필드를 찾을 수 없어서이다.


즉 이와같이 제네릭의 형태로 변경한다면 세가지 이점이 있다.

  • 컴파일단계에서 오류를 검출할 수 있다(중요)
  • 소스 중복의 제거
  • 타입의 안정성 추구

사실 제네릭은 옵저버패턴을 공부하다가 나온 개념인데, 생각보다 공부할게 많아서 포스팅이 늦어지고있다.


다음엔 제네릭 2차와 델리케이트를 포스팅해야겠다. 그다음이 옵저버ㅠ

================================================================