javac 컴파일러 - 제네릭, 애너테이션 프로세서

블러거·2026년 2월 3일

JVM

목록 보기
19/26

javac 컴파일의 편의문법 제거 대상인 제네릭과 개발자의 코드 추가 로직인 애너테이션 프로세서에 대해 알아보자.

제네릭

Object obj = "a";
Object[] strArr = new String[10]; // 공변
strArr[1] = 10; // 런타임 할당시 에러

배열은 공변이다. String 이 Object 의 하위 타입이라면 String[] 도 Object[] 의 하위 타입이다.
따라서 Object[] 정적 타입 변수에 String 값을 추가하는 것은 컴파일 타임에 에러를 낼 수 없다.

ArrayList list = new ArrayList();
list.add(Integer.valueOf(10)); // 컴파일, 런타임 에러 X
list.add("b"); // 컴파일, 런타임 에러 X

제네릭이 있기 전 ArrayList(컬렉션) 은 타입이 없다. 따라서 하나의 컬렉션에는 여러 타입을 넣을 수 있다.

제네릭 도입시 문제 - 하위 호환성

매개 변수화 된 타입인 제네레릭은 타입에 국한되지 않은 추상적인 코드를 만들 수 있게 한다.
하지만 위 처럼 제네릭이 있기 전의 컬렉션은 모든 타입을 받았다.

제네릭 컬렉션을 만들면서 기존 컬렉션 코드를 문제되게 하지 않으려면,

  1. 기존 컬렉션을 두고 새로운 제네릭 컬렉션을 새로 만든다.
  2. 기존 컬렉션을 제네릭 버전으로 변경하고, 런타임에는 타입 소거한다. 런타임에는 적절한 형변환 코드를 넣어서 해당 타입만이 들어감을 보장한다.

자바는 2번을 선택했다.
JDK 1.2 시절 Vector -> ArrayList, HashTable -> HashMap 과 같이 1번의 선택으로 컬렉션 라이브러리를 이미 변경한 탓에 또 1번을 선택하면 Vector<Int>, ArrayList<Int> 와 같이 너무 많은 클래스가 생겨 불편하기 때문이다.

이를 타입 소거 라고 한다.

타입 소거의 문제1 - 박싱/언박싱

제네릭 컬렉션에서 객체 타입을 써야하는 이유

하위 호환성을 위한 타입 소거로 인해 제네릭 컬렉션의 타입 제한은 컴파일 타임에 형변환 코드 가 자동 생성되어 동작한다.

문제의 원인은 자바 컬렉션 라이브러리는 내부에서 애초에 Object[] 를 사용했다는 것이다.
ArrayList 의 add는 아래 처럼 Object[] 에 값을 저장한다.
따라서 요소는 Object 의 하위 타입이어야 한다.

    private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length)
            elementData = grow();
        elementData[s] = e;
        size = s + 1;
    }

타입 변환 으로 제네릭을 구현한 이상 제네릭 컬렉션의 모든 요소는 Object 타입의 하위 타입이어야 한다는 결론이 난다.

이것이 제네릭에서 기본 타입을 쓰지 못하는 이유다. primitive 타입은 object 의 하위가 아니기 때문이다. 타입 소거 방식을 택할 수 밖에 없었기 때문에 발생한 문제다.

박싱/언박싱의 문제

다행히 원소로 객체 타입만을 쓸 수 있다는 문제는 코드 레벨에서는 없다.
primitive 타입을 원소를 add 하는 코드를 작성하더라도 박싱/언박싱을 자동으로 해주기 때문이다.

진짜 문제는 박싱/언박싱 자체가 느리고 양도 많아서, 자바 제네릭의 주요한 성능적 문제라는 것이다.

문제 해결의 노력 - 발할라 프로젝트

박싱/언박싱을 근본적으로 없애기 위해서는 다음 중 하나가 만족되어야 한다.

  • 런타임에 제네릭 타입을 유지(reified generics) 해서 List<int>와 List<Object>를 다른 클래스로 다루기
  • primitive마다 특수화된 버전(예: ListInt, ListDouble…)을 자동 생성
  • JVM의 값 타입(value type) 지원

이상의 방식은 모두 타입 자체를 유지하여 타입 소거가 되지 않도록 하는 방법이다. 외에도 여러 방식이 있다.

이 작업은 발할라 프로젝트라는 이름으로 진행중이다. 오랫동안.

타입 소거의 문제2 - 런타임에 엘리먼트의 타입을 알 수 없음

class Test {
	List<String> list;
}

위 list 의 제네릭으로 선언된 원소 타입 <String> 은 런타임에 알 수 없다.
타입이 소거되기 때문이다.
따라서 필요하다면 인자로 가지고 다녀야 한다.

다만 Signature 클래스 파일 속성에 제네릭 컬렉션 전체 타입이 적히긴 한다.
우린 리플렉션으로 이를 사용할 수 있다.

Field f = Test.class.getDeclaredField("list");
System.out.println(f.getGenericType());
// 결과 : java.util.List<java.lang.String>

getGenericType() 의 리턴값에서 String 만 따로 추출할 방법은 없다. 정말 필요하다면 파싱을 해야 한다.

타입 소거의 문제3 - 오버로딩 불가

public void sum(List<Integer> a) {}
public void sum(List<Double> a) {}

위 두 메서드의 시그니쳐는 분명히 다르지만 오버로딩이 불가하다.
런타임에는 둘의 타입 매개변수가 소거되므로 두 시그니쳐가 같아지기 때문이다.

애너테이션 프로세서

애너테이션 프로세서는 컴파일 동작을 개발자가 간섭할 수 있는 기능이다.
간단하게는 코드를 자동으로 추가하는 기능이며
정확하게 말하면 컴파일 타임에 추상 구문 트리를 수정하는 기능이다.

  • AbstractProcessor 를 구현하여 javac -processor [구현체 클래스] 로 실행한다.
  • 다음 등등의 동작이 가능
    • ProcessingEnvironment 의 Messager 로 컴파일시 에러 레벨을 지정하여 메세지를 출력
    • 에너테이션을 읽어서 값을 보고 메서드 생성(롬복)
    • 등등
  • 애너테이션 정보를 런타임에 사용하는 spring AOP 와 애너테이션 사용의 쌍두마차라 할 수 있다.
  • 대표적인 사용처로는 롬복, 하이버네이트 validator 등이 있다.
// 아래 애너테이션 둘다 필요
@SupportedAnnotationTypes("*") // 프로세서에 사용할 애너테이션 클래스 지정
@SupportedSourceVersion(SourceVersion.RELEASE_17)
public class CustomAnnotationProcessor extends AbstractProcessor {

	// 찾은 요소에 대해 실행할 클래스. 따로 적진 않을 예정
    // 예시의 구현은 ProcessingEnvironment 의 messager 를 이용한 컴파일 에러 출력
    private Validator validator;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        validator = new Validator(processingEnv.getMessager());
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        if (!roundEnv.processingOver()) {
        
        	// 프로세서 대상을 선택한다. 이 예시에서는 루트 요소만을 검사한다.
            // 애너테이션을 대상으로 하려면 getElementsAnnotatedWith() 를 사용한다.
            Set<? extends Element> rootElements = roundEnv.getRootElements();
            
            for (Element element : rootElements) {
                validator.validate(element);
            }
        }
        return false;
    }
}
profile
안녕하세요!

0개의 댓글