본문 바로가기

Programing/JVM(Java, Kotlin)

[Java] Generic in depth

2004년 JSR 176 규격에 의해 규정된 J2SE 5.0부터 자바에 제네릭(Generic)이라는 개념이 JSR 14로 추가되었다.

또한 많은 사람들이 Generic 타입이 추가 된 것을 5.0의 큰 변화로 꼽는다.

 

 

그런데 자바 카페나 블로그에 올라온 글들을 보면 근거 없는 정보들이 있어 확인이 필요했다.

 

1. 타입 이름은 아무 의미가 없다?

이 주장과 아래 2번 주장은 'justkukaro님의블로그'에서 발견했다.

위의 주장은 문법상으로는 맞는 말이다. 하지만 일상 생활을 비유로 들자면 우리는 법만 지키고 살면된다라는 말과 다를 바가 없다. 이 법이라는 것은 도덕이라는 개념과 대치되는 개념으로 사용되었다. 만약 (도덕이라는 다른 범주를 무시하고) 법만 지키면서 살면 힘든 세상이 될 것이다.

일상 생활을 컴퓨터 세상으로 매핑을 해보면, 

언어에는 법과 같은 문법이 있고, 도덕과 같은 컨벤션(convention)이라는 것이 존재한다.

: 프로그래밍 문법(syntax) => 컴파일러가 보통 체크를 한다.

도덕: 프로그래밍 컨벤션(관습/관례) => 사람(개발자)을 위해 중요하다.

오래되어서 아카이브 되었지만 자바 컨벤션 이 괜히 있는 것이 아니다.

 

위의 주장에 대해 반박하는 근거는 Java Tutorial의 타입 파라메터에 대한 네이밍 컨벤션을 통해 쉽게 찾을 수 있다.

자바에서 E를 많이 쓰게 된 것에는 그냥 우연히 벌어진 결과가 아니다. E가 위치한 곳에는 원소(Element)를 받는 곳이기 때문에 머릿글자 E를 쓰는 것이다.

T를 쓰는 경우는 C++을 하다 넘어와서가 아니고 타입(Type)이라서 그렇게 적은 것이다.

결국적으로 타입 파라메터는 나름 규칙이 있는 알파벳이라고 결론 내릴 수 있다.

 

2. Raw type은 Object형을 받지 않는다?

대부분의 교재(아마도 책?)에서 잘못 쓴 것 같다고 하는데, 오히려 게시글의 저자가 잘못알고 있는 것 같다.
판단하게 된 근거가 궁금하다. (댓글로 질문을 남겼으나 댓글을 지워버려서 알 수 없었다.)
이 내용은 아래 3번 질문 이후에 같이 살펴본다.

3. Generic은 타입 캐스팅(형변환)을 하지 않고 사용하게 해준다?

그렇다면 Generic 을 쓰면 casting 을 안하게 되어 성능상의 이점을 얻을 수 있을까?

출처: 미디엄 - 슬기로운 개발생활

 

일단 애플리케이션 개발자 입장에서는 맞는 이야기이다. 하지만 JVM 전체 시스템에 관점으로 관점을 넓혀보면 어떨까?

 

검증

2번과 3번 질문에 답변을 위해, 확인을 위해 간단히 코드를 작성해 보았다.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;

public class Student {
    private static final Logger log = LoggerFactory.getLogger(Student.class);

    private int age;

    public int getAge() {
        return this.age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" + "age=" + age + '}';
    }

    public static void main(String[] args) {
        callByUsingGeneric();
        callByUsingRawType();
    }

    private static void callByUsingGeneric() {
        Student student = new Student();
        student.setAge(11);

        List<Student> studentList = new ArrayList<>();
        studentList.add(student);

        Student firstStudent = studentList.get(0);
        log.info("firstStudent: {}", firstStudent);
    }

    @SuppressWarnings("unchecked")
    private static void callByUsingRawType() {
        Student student = new Student();
        student.setAge(11);

        List studentList = new ArrayList();
        studentList.add(student);

        Student firstStudent = (Student)studentList.get(0);
        log.info("firstStudent: {}", firstStudent);
    }
}

callByUsingGeneric와  callByUsingRawType 메서드로 동일한 코드를 하나는 제너릭을 사용하고, 하나는 RawType으로 사용을 하였다.

위에서 언급했지만 코드 상에는 분명히 캐스팅을 하는 부분이 있고 없고의 차이가 있다.

 

그렇다면 자바 컴파일러에 의한 바이트코드도 다를까?

바이트코드 뷰어로 확인해보면 아래와 같다.

 

Java 코드의 라인이 달라서 많은 diff가 보인다. 하지만 무시하고 캐스팅 하는 코드를 살펴보면 사실 동일한 코드로 바뀜을 알 수 있다.

노란박스에서 보듯이 바이트 코드상에서는 두 경우 모두 CHECKCAST라는 명령에 의해 형변환을 하고 있음을 알 수 있다.

 

 Type Erasure(타입 소거)

이렇게 되는 이유를 이해하기 위해서는 Type Erasure(타입 소거)라는 개념을 이해할 필요가 있다.

Java Tutorials > Generics > Type Erasure에 잘 설명이 되어 있다.

Generics은 기존 과거의 버전(5.0 이전)과 호환이 되도록 구현을 위해, 자바 컴파일러는 다음과 같은 원칙에 의해 소거 작업을 한다.

  • 모든 타입 파라미터들은 bound 되었을 경우바운드 타입으로, bound 되지 않았을 경우에는 Object로 바뀌게 된다.
  • 타입 안정성을 보장하기 위해 필요시 타입 캐스트를 삽입한다.
  • 다형성을 보존하기 위해 확장된 제너릭 타입의 경우 브릿지 메서드를 생성한다.

여기에 대한 설명은 튜토리얼이나 baeldung.com > Type Erasure in Java Explained 에 잘 설명되어 있다.

 

타입 소거로 인해 발생하는 문제와 Super type token

JSON와 객체 간에 변환을 할 경우 Generic 으로 사용한 타입이 실제 런타임시에는 알 수가 없다.

따라서 변환을 할 때 CastClassCastException 가 발생하여 문제가 생길 수 있다.

관련해서는 Super type token 이라는 편법(?)을 사용하는 방법이 존재한다.

익명 내부 클래스에서는 타입이 제거되지 않는 자바의 특징을 이용하는 방법이다.

관련해서는 [Gson] List 타입 추론 방법이라는 글에 적어두었다.