Java/Java 를 파헤쳐보자

[Java 파헤쳐보기] Generic (제너릭)

동구름이 2023. 2. 23. 00:44

 Java를 공부하며 Generic의 개념과 자료 구조에서 사용하는 것 정도는 어렴풋이 알겠는데, 정확히 왜 필요한지가 궁금했습니다. 이번 포스팅에서 제너릭을  다루어보겠습니다.

 

 

제너릭이란?

 제너릭(Generic)을 영어 단어 그대로 직역하자면, 클래스 또는 사물 그룹의 특징 또는 이와 관련된 일반적이라는 것이라는 뜻입니다.

 

 자바에서 사용하는 제너릭의 의미도 위와 비슷합니다.

 

 자바에서 제너릭이란, 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 의미합니다.  그래서 구체적인  타입에 대한 정보를 타입의 인스턴스화 시점에 전달해, 객체별로 다른 타입의 자료가 저장될 수 있도록 하는 것입니다.

 

 

제너릭의 역할

 자바에서는 제너릭을 통해, 다양한 타입의 객체를 다루는 메서드나 클래스에 대해서 컴파일 타임 타입 체크를 가능하게 하여 타입 안정성을 높이고 형 변환의 번거로움을 줄여줄 수 있습니다.

 

 제너릭이 없을 때와 있을 때의 두 가지 상황을 예시를 통해 쉽게 이해할 수 있습니다.

 

차와 버스에 필요한 handle을 생산하는 공장을 예시로 들어보겠습니다.

 

 

제너릭이 없을 때

우선 제너릭이 없을 때를 살펴보겠습니다.

 

아래는 Car, Bus 클래스입니다.

//Car 클래스
public class Car {
    private String name;
 
    public Car(String name) {
        this.name = name;
    }
 
    public String getName() {
        return name;
    }
}

//Bus 클래스
public class Bus {
    private String number;
 
    public Bus(String number) {
        this.number = number;
    }
 
    public String getNumber() {
        return number;
    }
}

 

 

그리고 두 클래스에 조립할 Handle을 찍어내는 클래스는 아래와 같습니다.

public class HandleController {
    private Object assembledVehicle;
 
    public HandleController(Object assembledVehicle) {
        this.assembledVehicle = assembledVehicle;
    }
 
    public Object getAssembledVehicle() {
        return assembledVehicle;
    }
}

 

 

그리고 위의 클래스들을 통해 Car와 Bus에 맞는 Handle을 아래처럼 조립할 수 있습니다.

Car car = new Car("차");
Bus bus = new Bus("버스");
HandleController carHandleController = new HandleController(car);
HandleController busHandleController = new HandleController(bus);

 

 

 

이제부터 헷갈리는 부분이 생깁니다. 각각에 맞는 핸들을 조립해 사용한다고 생각해보겠습니다.

Object assembledVehicle1 = carHandleController.getAssembledVehicle();
Car assembledCar = (Car)assembledVehicle1;
System.out.println(assembledCar.getName());
 
Object assembledVehicle2 = busHandleController.getAssembledVehicle();
Bus assembledBus = (Radio)assembledVehicle2;
System.out.println(assembledBus.getNumber());

 

이렇게 코드를 짜기 위해 개발자들이 숙지해야할 것은 carHandleController 가 car와 연결되어있다는것,  그리고 assembledVehicle1이 Car라는 것을 숙지하고 있어야 한다는 것, 그리고 그에 맞게 (Car) 명시적 타입 캐스팅을 통해 객체를 사용하는 것이 있습니다.

 

 

 만약 여기서 하나라도 잘못 숙지를 한다면, 다음과 같은 코드가 생깁니다.

Object assembledVehicle1 = carHandleController.getAssembledVehicle();
Bus assembledBus = (Bus)assembledVehicle1;
System.out.println(assembledBus.getNumber());

 

assembledVehicle1 라는 객체는 차와 연관있지만, Bus로 형변환을 시키는 것입니다. 그러면 ClassCastException이 발생하게 됩니다.

 

 

 

여기서 생각해볼 수 있는 문제의 원인은 carHandleController에 연결된 객체가 무엇인지 개발자가 인지하기 힘들었다는 것입니다.

 

 

이것을 제너릭을 사용한 예시로 다시 살펴보겠습니다.

 

제너릭이 있을 때

public class HandleController<Vehicle> {
    private Vehicle assembleVehicle;
 
    public HandleController(Vehicle assembleVehicle) {
        this.assembleVehicle = assembleVehicle;
    }
 
    public Vehicle getAssembleVehicle() {
        return assembleVehicle;
    }
}

//이전 코드
//public class HandleController {
//    private Object assembledVehicle;
// 
//    public HandleController(Object assembledVehicle) {
//        this.assembledVehicle = assembledVehicle;
//    }
//
//    public Object getAssembledVehicle() {
//        return assembledVehicle;
//    }
//}

 

 위의 클래스에서 Vehicle이라는 타입 변수가 지정된 것을 볼 수 있습니다. 이를 통해 클래스에 대한 변수를 정의할 수 있습니다. 

 

 

Car car = new Car("차");
Bus bus = new Bus("버스");
HandleController<Car> carHandleController = new HandleController<Car>(car);
HandleController<Bus> busHandleController = new HandleController<Bus>(bus);

//이전 코드
//HandleController carHandleController = new HandleController(car);
//HandleController busHandleController = new HandleController(bus);

 

제너릭을 통해 각각의 차와 버스를 만들어낸 모습입니다.

 

 

이렇게 제너릭 클래스로 선언되면 아래처럼 사용할 수 있습니다.

Car assembledCar = carHandleController.getAssembledVehicle();
System.out.println(assembledCar.getName());
 
Bus assembledBus = busHandleController.getAssembledVehicle();
System.out.println(assembledBus.getNumber());

//이전 코드
//Object assembledVehicle1 = carHandleController.getAssembledVehicle();
//Bus assembledBus = (Bus)assembledVehicle1;
//System.out.println(assembledBus.getNumber());

 

 가장 크게는 형 변환이 없어진 것을 볼 수 있습니다. 그래서 만약 Car와 Bus를 혼용하더라도 두 가지 객체는 서로 타입 변환이 되지 않기 때문에, 예외 처리 이전에 컴파일러에서 컴파일 오류를 내게 됩니다.

 그리고 타입 형태를 지정해 제공한다는 것은 코드를 재사용하는 것에도 큰 추가 이점이 됩니다. 

 

 

 

 

 

 

 

 

 

참고자료

Java Generic 을 파헤쳐보자 - 개념편
https://dev.gmarket.com/12

제네릭(Generic)의 이해
https://st-lab.tistory.com/153