Java

[Java] 지네릭스(Generics)

on1ystar 2023. 3. 27. 10:02
728x90
반응형

지네릭스(Generics)란?

지네릭스는 JDK1.5에서 처음 도입된 문법으로 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크를 해주는 기능이다.

자바는 객체 지향 개념 중 하나인 다형성이란 개념이 있다. 이 다형성을 자바에서는 형변환으로 크게 지원하고 있는데, 이 형변환은 프로그래머에게 큰 편리함을 주지만 컴파일러에게는 큰 약점이 된다.

컴파일러는 변수 안에 담겨 있는 실제 값을 확인하지 않는다. 단지 참조 변수의 타입을 보고 문법적 오류가 있는 지를 판단한다. 이것이 컴파일러의 한계인데, 다형성에서 이로 인한 런타임 에러가 발생할 수 있다.

하나의 예시를 들어보자.

import java.util.ArrayList;

public class GenericsTest {
    public static void main(String[] args) {
        ArrayList list = new ArrayList();
        list.add(10);
        list.add(20);
        list.add("30"); 

        Integer i = (Integer)list.get(2);
    }
}

프로그래머는 ArrayList 타입 변수에 Integer 타입의 값들만 넣기를 원한다. 하지만 ArrayList 클래스는 내부적으로 Object 타입의 배열로 값들을 저장하기 때문에 모든 타입의 변수를 저장할 수 있다. 이 결과 프로그래머의 실수로 인해 String 타입의 값이 들어가게 됐지만, 컴파일은 하위 클래스 타입의 값을 저장하는 것은 문제가 없으므로 문제가 없는 것 처럼 보인다.

하지만 코드를 실행하면 아래와 같은 형변환 예외가 발생한다.

java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')

list.get(2)로 꺼내진 값은 String 타입의 “30”이다. 이를 Integer 타입으로 형변환을 하려 했기 때문에 런타임 에러가 발생한 것이다. 하지만 컴파일은 참조 변수가 Object 타입이기 때문에 이를 Integer 타입으로 형변환 하는 것은 문제가 없다고 판단해 버린다.

Object 타입은 모든 클래스들의 상위 클래스기 때문에 보통 컴파일 시 문제가 되지 않는다. 안에 어떤 값이 담겨있는지 컴파일은 알 수가 없다. 따라서 위와 같이 프로그래머가 의도치 않게 다른 값을 넣어도 컴파일 시 에러가 발생하지 않고 실행 시 에러가 발생해 프로그램이 죽는 치명적 결함이 된다.

자바는 1.5 버전 이후부터 지네릭스를 도입해 위와 같은 문제를 해결하려 했다. 즉 실행 시 발생할 수 있는 에러를 컴파일로 가져와 프로그래머가 알아차리게 한 후, 수정할 수 있게하기 위함이다.

지네릭스(Generics)는 런타임 에러를 컴파일 에러로 바꾸기 위한 결과물

 

지네릭스는 2가지의 장점을 가지고 있다.

  • 타입 안정성을 제공한다.
  • 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해 진다.

타입 안정성을 높인다는 것은 의도하지 않은 타입의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여준다는 뜻이다.

import java.util.ArrayList;

public class GenericsTest {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<Integer>();
        list.add(10);
        list.add(20);
        // list.add("30");  // The method add(Integer) in the type ArrayList<Integer> is not applicable for the arguments (String)
        list.add(30);

        Integer i = list.get(2);  // 형변환 생략

    }
}

위와 같이 리스트에 저장할 값은 Integer라고 컴파일러에게 타입 정보를 주게 되면 컴파일러는 list.add("30");가 잘못된 문장이라는 것을 알아차릴 수 있게 된다.

어떻게 보면 저장할 수 있는 타입을 강제화하는 것이다. 모든 프로그래밍 문법에서 그렇듯, 자유성, 유연성을 줄이는 대신 안정성을 높이는 방법이다.

지네릭 클래스와 타입 변수

지네릭스가 자바에 도입되면서 내부에 Object 타입을 포함했던 기존의 일반 클래스들을 지네릭스를 사용해 지네릭 클래스로 바꿨다. 대표적인 지네릭 클래스가 ArrayList 클래스다. 구현된 방법을 공식 API 문서에서 찾아볼 수 있다.

package java.util;
public class ArrayList<E> extends java.util.AbstractList implements java.util.List, java.util.RandomAccess, java.lang.Cloneable, java.io.Serializable {
	// ... 생략

	public boolean add(E e) {
	  // ...
	}

	public E get(int index) {
		// ...
	  // the element at the specified position in this list
  }
}

자세한 구현 코드는 볼 수 없지만 대략적으로 어떻게 구현되어 있는 지는 알 수 있다.

  • public class ArrayList<E>

클래스 옆에 <E> 를 붙여 지네릭 클래스로 변경했다. 여기서 E타입 변수(type variable)라고 한다. 보통 Type이나 Element에서 앞 글자를 따와 대문자 T나 E를 많이 사용한다. 타입 변수는 임의의 참조형 타입을 의미하고 있다.

또한 타입 변수는 타입 매개 변수라고도 한다. 지네릭 클래스의 인스턴스를 생성할 때 타입을 지정해 줘야 하는데, 마치 매개 변수에 값을 넣는 것처럼 타입을 대입한다는 의미다.

public class GenericsTest {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<Integer>();

위처럼 타입 변수 EInteger를 대입하면, 생성된 인스턴스의 멤버들은 컴파일 후 모든 타입 변수 EInteger로 바뀌게 된다.

  • public boolean add(E e)
  • public E get(int index)

에를 들어 위 두 함수를 보면 매개 변수와 반환 타입에 타입 변수 E를 사용했다. 프로그래머가 위처럼 ArrayList 인스턴스를 생성할 때 Integer 타입을 대입했다면 실제 코드는 컴파일 후 아래와 같이 바뀌게 된다.

  • public boolean add(Integer e)
  • public Integer get(int index)

지네릭 클래스와 다형성

지네릭 클래스를 이용하여 인스턴스를 생성할 때는 항상 참조변수와 생성자에 대입된 타입이 일치해야 한다.

  • ArrayList<Integer> list = new ArrayList<Integer>(); 일치
  • ArrayList<Object> list = new ArrayList<Integer>(); 불일치 → 에러

단, 참조 변수는 다형성이 허락하는 내에서는 일치하지 않아도 된다.

  • List<Integer> list = new ArrayList<Integer>();

ArrayListList 인터페이스를 구현했기 때문에 참조 변수의 다형성에 문제되지 않는다.

더 쉬운 예를 들어보면 아래와 같은 코드도 가능하다.

class Fruit<T> {}
class Apple<T> extends Fruit {}

public class GenericsTest {
    public static void main(String[] args) {
				Fruit<Object> myFruit = new Apple<Object>();
        List<Fruit> list = new ArrayList<Fruit>();
        
        list.add(new Apple());

    }
}

또한 위 예제에서는 list에 대입된 타입이 Fruit이기 때문에 다형성에 의해 Apple 인스턴스를 add 메서드로 추가할 수 있다.

 

References

자바의 정석 유튜브 강의 - 남궁성의 정석코딩

https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/ArrayList.html

728x90
반응형