자바

[Java] 자바 제네릭, Generic 문법

whiporithm 2023. 10. 3. 21:15

 

기존 프로젝트를 리팩토링 하다가 제네릭 문법을 만났다. 어떤 의미로 사용하는지는 알지만, 제대로 알고 사용한게 아니라서 다시 보는데도 너무 헷갈렸다. 생각해보니 제네릭을 책으로 제대로 공부한 적이 없어서.. 책장에 있는 책을 참고해서 제네릭 클래스와 메소드 정도를 가볍게 정리해보려 한다.

제네릭의 의미와 필요성

제네릭의 뜻은 '일반화' 이다. 그리고 자바에서 말하는 일반화의 대상은 자료형을 의미한다. 간단히 말하면 특정 클래스나 메소드를 사용하는데에 있어서 자료형의 구속에서 벗어날 수 있는 것이다.

 

자바는 객체지향 언어이니 클래스를 기준으로 설명해보겠다. 

클래스가 총3개가 있다고 해보자, '사과' , '오렌지' , '박스' 이다. 이 때 현실적으로 박스에는 사과나 오렌지, 어떤게 들어가도 큰 상관이 없어야 한다. 그러나 문법적으로는 필드를 가질때 자료형을 명시해줘야 한다. 아래와 같이 말이다.

 

public class Box {

    private Apple apple;
    
    setBox(Apple apple){
    	this.apple = apple;
    }
    
    getBox(){
    	return this.apple;
    }
}

 

이렇게 될 경우 박스 클래스는 '사과' 만 담는 박스가 된다. 이를 해결하기 위한 다른 방법은 필드의 데이터를 Object로 나타내는 것이다. Object는 최상위 클래스로 모든 클래스를 자식으로 받아들일 수 있기 때문이다.

 

public class Box {

    private Object fruit;
    
    setBox(Object fruit){
    	this.fruit = fruit;
    }
    
    getBox(){
    	return this.fruit;
    }
}

 

단  Object로 했을 경우에 문제점이 여러가지 발생한다.

 

1. 데이터를 꺼낼때 형 변환을 해줘야 한다. 상속의 개념으로, 부모 클래스로 데이터를 받은 이후에 하위 클래스의 메소드나 필드를 사용하기 위해서는 클래스의 형 변환이 필요하다.

 

Box aBox = new Box(new Apple());
Box oBox = new Box(new Orange());
Apple ap = (Apple)aBox.getBox();
Orange or = (Orange)oBox.getBox();

 

2. 다른 클래스가 들어가도 컴파일 단에서 에러를 체크할 수 없다. 만약 Apple 객체가 아닌 문자열이 들어갔어도 컴파일 단계에서는 문법적으로 오류를 잡을 수 없기에, 런타임 에러로 이어진다.

 

Box box = new Box();
box.setBox("this is apple"); // 에러발생 x (Object는 모든 클래스의 부모)
Apple ap = (Apple)box.getBox(); // 런타임에서 에러가 발생한다.

 

위와 같은 이유들로 Object를 사용하면 자료형을 컨트롤 할 수 없을 수 있다. 더불어 2번째 예시 같은 경우에는 런타임에서 에러가 나기라도 했지만, 설계 오류로 런타임에서도 에러가 안나고 더 큰 이슈가 생길수도 있다.

제네릭 클래스

위에서 정의한 Object 기반 Box 클래스를 제네릭 클래스로 표현하면 아래와 같다.

 

class Box<T> {
    private T fruit;
    
    public void setBox(T fruit){
    	this.fruit = fruit;
    }
    
    public T getBox(){
    	return fruit; 
    }
}

 

위에 나오는 키워드 T가 자료형에 의존적이지 않고, 모든 클래스를 받아들인다는 의미이다. (참고로 T가 아니고 다른 알파벳으로 대체 가능하다. T는 보통 Type을 의미해서 통상적으로 사용되는 것이다.)

클래스 뒤에 <T>를 붙여주는 이유는 이 클래스가 제네릭 문법을 사용하는 클래스임을 명시해주는 것이다. (만약 이게 없다면 우리가 T라는 클래스를 만들수도 있는데 혼동될 수 있기 때문이다.) 

 

그리고 자료형을 명시해서 어떤 과일을 담을 박스일지 "인스턴스 생성 시" 에 정해줄 수 있다. (T가 어떤 자료형인지 정해진다.)

 

Box<Apple> aBox = new Box<Apple>();
Box<Apple> aBox = new Box<>(); // 타입 추론으로 생략 가능

 

위처럼 <> 안에 자료형을 명시함으로써 어떤 박스인지 문법적으로 컴파일러가 해석할 수 있다. 그리고 변수를 선언할 때 자료형을 명시해주면 뒤에 인스턴스를 생성할 때는 컴파일러가 자료형을 추론해주므로 생략도 가능하다.

 

자, 이렇게 자료형을 명시해줌으로써 런타임에서 나는 에러를 컴파일 단계에서 잡을 수 있다.

 

Box<Apple> box = new Box<>();
box.setBox("this is apple"); // 컴파일 단계에서 에러!

 

현재는 사과를 담는 박스인데 뜬금없이 문자열이 들어왔으니 컴파일 단계에서 에러를 낼 수 있고, 프로그래머는 빠르게 에러를 수정할 수 있다.

 

다중 매개변수 제네릭 클래스

위에서는 T 로 한 개의 자료형을 대체하였는데 두 개 이상의 자료형을 제네릭으로도 표현할 수 있다.

 

class DBox<L, R> {
    private L left;
    private R right;
}

 

제네릭을 표현하기 위해 알파벳 (또는 다른 문자) 은 무엇을 사용해도 상관 없으나, 보편적으로 사용되는 의미로 사용하는것이 좋다.

 

E Element
K Key
N Number
T Type
V Value

 

제네릭 클래스 타입 인자 제한

위에서는 박스라고 칭했지만, 만약 우리가 '과일' 만 담는 과일박스를 사용하고 싶다면, 박스에 들어오는 물건들을 보고 적당히 쳐내야 할 것이다. 갑자기 박스에 책이 들어가거나 하면 안되지 않는가.

 

Box box<Book> = new Box<>(); // 과일만 담고 싶은데 Book도 들어가니 문제!

 

위와 같은 상황이 싫단 말이다. 이런 상황에 대비하기 위하여 제네릭 클래스에서 extends 키워드로 타입 인자를 제한할 수 있다.

 

interface Fruit {
    public String eat();
}

class Apple implements Fruit {
    public String toString() {
    	return "I am Apple";
    }
    
    @Override
    public String eat() {
    	return "apple, Yummy!";
    }
}

class Box<T extends Fruit> {
    T fruit;
    
    ...
}

 

위에서는 인터페이스를 활용하여 인자를 제한해보았다. 위와 같이 선언하면 특정 인터페이스를 구현하는 클래스들만 Box의 자료형으로 생성할 수 있는 것이다.  그리고 Box는 Fruit으로 제한했기에 내부 클래스에서 Fruit의 함수를 호출할 수 있다. (eat) 

 

이렇게 선언한뒤 Book 으로 자료형으로 설정할려고 하면 컴파일에서 에러가 발생해서 더욱 효율적으로 코드를 작성할 수 있다.

제네릭 메소드

제네릭 문법은 비단 클래스 뿐 아니라, 메소드에도 적용이 가능하다. 아래의 코드를 보자.

 

public static <T> Box<T> makeBox(T o) { ... }
public <T> void func(T o) { ... }

 

제네릭 메소드는 클래스 내부의 메소드로도 선언 가능하고, 전연적으로 사용하는 static 메소드로도 선언이 가능하다.

클래스와 같은 이유로 T가 제네릭 문법을 의미함을 알려주기 위해서 접근자 뒤에 <T>와 같이 선언한다.

 


 

참고 : 윤성우의 열혈 JAVA 프로그래밍