Generic (1)

혁콩·2024년 5월 8일
0
post-thumbnail

들어가기에 앞서

새로운 기술이 등장할땐 항상 그 배경에 어떠한 이유가 있다.
코루틴과 같이 적은 리소스로 스레드를 동작시키기 위해 Java 21에 등장한 Virtual Thread 라던지, 기존 EJB의 복잡성과 개발성을 크게 향상시키며 자바의 새로운 봄을 가져온 Spring Framework와 같은 것들 말이다.

Generic, 제네릭은 JDK 1.6버전에 등장한 새로운 개념이다. 그렇다면, 제네릭은 과연 어떠한 문제점을 해결하기 위해 등장했으며, 어떻게 해결하는지, 어디에 사용되는지를 생각하며 공부한다면 좋을 것이다.

등장 배경

제네릭이 가장 많이 사용되는 곳을 말해보라면, 필자는 Collection Framework를 고를 것이다. 어떻게 사용되는지 잠깐 확인해보자.

public interface List<E> extends SequencedCollection<E> {
	...
}

꺽쇠 안에 보이는 <E> 를 통해 다양한 형태의 자료형들을 받을 수 있다.

// Integer
List<Integer> list = new ArrayList<>();
// 혹은 String
List<String> list = new ArrayList<>();

제네릭이 존재하지 않는다고 가정해보자. Collection Framework 처럼 다양한 자료형을 처리하기 위해선 Objectinstanceof 연산자를 이용한 형변환이 필요할 것이다.

public class Something {
	Object object;
    public Something(Object object) {
		this.object = object;
	}
    public void set(Object object) {
    	this.object = object;
    }
    public Object get() {
    	return object;
    }
}

public class Main {
	public static void main(String[] args) {
    	Something intValue = new Something(10);
    }
}

추후에 값 연산이 진행되어야 하는 요구사항이 생겼다.

public class Main {
	public static void main(String[] args) {
    	Something intValue = new Something(10);
        Integer number = (Integer) intValue.get();
        	...
    }
}

위와 같이 프로그램을 작성하고 실행을 했더니..

타입 캐스팅 중 예외가 발생했다.

public class Main {
	public static void main(String[] args) {
    	Something intValue = new Something("10");
        Integer number = (Integer) intValue.get();
        	...
    }
}

누군가가 위와 같이 String을 넣어버린 것이다. 해결해보자

```java
public class Main {
	public static void main(String[] args) {
    	Something intValue = new Something(10);
        if (intValue instanceof Integer) {
        	Integer number = (Integer) intValue.get();
        }
        	...
    }
}

instanceof 연산자를 활용해 타입을 체크하고 캐스팅을 진행했다. 이제 문제가 없을까? 예외는 발생하지 않지만, 의도한 동작이 전혀 진행되지 않는다.

문제점

위 예시에선 두가지 큰 문제점이 있다.

첫번째는 위에서 봤던것과 같이, 캐스팅 시 RuntimeException이 발생할 수 있다는 것이다. 이는 언제 어디서 발생할 지 모르며, 프로그램의 의도하지 않은 동작을 발생시킬수도 있다.

두번째는 Object Type이므로 Type Casting, 즉 형변환이 강제된다는 것이다. 이 과정에서 타입을 체크하고, 캐스팅하는 코드들이 반복되며, 생산성 / 가독성 / 유지보수성에 좋지 않은 영향을 끼친다.

제네릭은 과연 어떤 원리로 위와 같은 문제점들을 해결한걸까?

원리

제네릭은 형 변환 시 발생할 수 있는 예외를 컴파일 시점에서 확인하기 위해 개발되었다. List 인터페이스를 다시 확인해보자.

public interface List<E> extends SequencedCollection<E> {
	...
}

Parameterized Type

하나 이상의 타입 파라미터를 선언하고 있는 클래스나 인터페이스를 제네릭 타입 이라고 부른다.

List<E>E와 같은 것들을 형식 타입 매개변수(formal type parameter) 라고 부르며, 구현 시 등록되는 실제 타입을 실 타입 매개변수(actual type parameter) 라고 부른다.

public interface List<E> { ... } // 형식 타입 매개변수

List<String> list = new ArrayList<>(); // 실 타입 매개변수

Type Erasure

제네릭은 컴파일 시 Type Erasure(타입 소거자) 에 의해 자신의 타입 요소 정보를 삭제한다.

타입을 굳이 지워야하나요?

라는 생각도 들었지만, 이를 통해 이전 버전의 JDK와의 호환성 문제를 해결하고, 복잡성을 감소시킨다고 한다.

런타임 시 타입이 존재하지 않으면 문제가 발생하지 않나요?

제네릭 이전의 문제점 중 하나는 Runtime Exception 발생 이라고 했다.
제네릭은 컴파일 시 타입을 체크하기에, 컴파일러가 문제가 없음을 보장한다.

List<String> list = new ArrayList<>();

List list = new ArrayList(); // 컴파일 후엔 다음과 같이 변함
다만, Unbound wildcard의 경우 타입이 소거되지 않는다. 이는 나중에 알아보자.

장단점

장점

타입 캐스팅에 대한 오류를 컴파일 시 확인할 수 있게 된다

제네릭을 사용하는 이유 중 가장 큰 지분을 차지한다고 생각한다.
가장 좋은 예외는 컴파일 예외 라는 유명한 말도 있으니..
그 외 아래와 같은 장점들이 있다.

  • 타입에 의존하지 않는 로직의 수행이 가능하다
  • 동일한 코드를 여러 타입에 적용할 수 있어 재사용성 증가

단점

List<int> list = new ArrayList<>(); // 이게 아니고
List<Integer> list = new ArrayList<>(); // 이렇게 사용한다

위 예시에서 볼 수 있듯이, 제네릭을 사용할 땐 primitive type은 사용할 수 없다.
다만, Wrapper 클래스를 사용하면 되기에 큰 단점이라고는 느껴지지 않는다.

추가적인 단점으론 다음과 같은 것들이 있다.

  • 제네릭 타입의 인스턴스 생성 불가
  • type parameter의 구분으로 overload는 불가
    type erasure로 인해 런타임 시 정보가 사라져 같은 형태가 되기 때문

마치며

자바를 다루다보면 반드시 마주치게되는 제네릭의 원리와 장단점에 대해 알아보았다.
wildcard에 대한 내용을 추가하다 보니, 글이 너무 길어진 느낌이라 해당 내용은 다음 포스트에서 정리하겠다.

참고 자료

java generic
type erasure
generic guide
stack overflow - what is pecs

profile
아는 척 하기 좋아하는 콩

0개의 댓글