제네릭의 타입 소거
Spring에서 런타임에 제네릭 타입 매개변수를 가져오는 방법
2025-03-01
이번에 Spring 공식 문서를 읽으면서 Java 언어 명세에 대한 의문점이 들었습니다.
의문점이 든 부분은 Spring의 @Qualifier에 대한 내용이었습니다.

Qualifier

Spring에서는 의존성을 주입받을 때 여러 후보 Bean이 있는 경우 어떤 Bean을 주입받을지 @Qualifier를 통해 명시할 수 있습니다.
UserService.java
@Service
public class UserService {
    private final UserRepository userRepository;
 
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}
위 코드에서는 UserRepository 타입을 가진 Bean이 하나만 존재한다면 정상적으로 작동하지만, 여러 개이 존재한다면 NoUniqueBeanDefinitionException이 발생하게 됩니다.
TestContainer.java
@Service
public class UserService {
    private final UserRepository userRepository;
 
    public UserService(@Qualifier("userRepository") UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}
그래서 여러 후보 Bean이 존재하는 경우, @Qualifier를 통해 주입받고자 하는 Bean을 명시해야 합니다.

제네릭

Spring은 단순히 이름 뿐만 아니라 제네릭(Generic)의 타입 매개변수를 통해서 Bean을 구별할 수도 있습니다.
RedisConfiguration.java
@Configuration
public class RedisConfiguration {
    @Bean
    public RedisTemplate<String, String> redisTemplate1() {
        return new RedisTemplate<>();
    }
 
    @Bean
    public RedisTemplate<Integer, String> redisTemplate2() {
        return new RedisTemplate<>();
    }
}
UserService.java
@Service
public class UserService {
    private final RedisTemplate<String, String> redisTemplate;
 
    public TestContainer(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
}
위 코드에서는 RedisTemplate 타입을 가진 Bean이 두 개 존재하지만 NoUniqueBeanDefinitionException이 발생하지 않고 정상적으로 의존성 주입이 됩니다.
이는 Spring에서는 제네릭 타입 매개변수를 통해서 Bean을 식별할 수 있기 때문입니다.


그러나 Spring이 Bean을 생성하는 과정은 런타임 시점에 수행되는데요.
Spring은 어떻게 런타임에 제네릭 타입 매개변수를 참조해 Bean을 식별했는지 의문이 생겼습니다.

제네릭 타입 매개변수

JLS(Java Language Specification)에는 제네릭에 사용되는 타입 매개변수 등은 컴파일 시점에 삭제된다고 설명되어 있는데요.
그러나 앞서 설명드렸듯이, Spring에서는 런타임에 제네릭 타입 매개변수를 참조하고 있습니다.
게다가 리플렉션(Reflection)에는 getGenericReturnType() 등의 제네릭 타입 매개변수를 포함한 전체 타입을 가져오는 API(Application Programming Interface)도 있었습니다.

Signature

여러 문서를 찾아보던 중, JVM(Java Virtual Machine) 명세에서 관련된 내용을 찾았는데요.
JVM 명세에 따르면 Java 컴파일러는 타입 매개변수를 사용하는 클래스나 메서드 등의 선언부에 대해 서명(Signature)을 생성해야 한다고 명시되어 있었습니다.
RedisConfiguration.class
// class version 65.0 (65)
// access flags 0x21
public class com/example/demo/config/RedisConfiguration {
  ...
  // access flags 0x0
  // signature ()Lorg/springframework/data/redis/core/RedisTemplate<Ljava/lang/String;Ljava/lang/String;>;
  // declaration: org/springframework/data/redis/core/RedisTemplate<java.lang.String, java.lang.String> redisTemplate1()
  redisTemplate1()Lorg/springframework/data/redis/core/RedisTemplate;
  @Lorg/springframework/context/annotation/Bean;()
   L0
    LINENUMBER 13 L0
    NEW org/springframework/data/redis/core/RedisTemplate
    DUP
    INVOKESPECIAL org/springframework/data/redis/core/RedisTemplate.<init> ()V
    ARETURN
   L1
    LOCALVARIABLE this Lcom/example/demo/config/RedisConfiguration; L0 L1 0
    MAXSTACK = 2
    MAXLOCALS = 1
 
  // access flags 0x0
  // signature ()Lorg/springframework/data/redis/core/RedisTemplate<Ljava/lang/Integer;Ljava/lang/String;>;
  // declaration: org/springframework/data/redis/core/RedisTemplate<java.lang.Integer, java.lang.String> redisTemplate2()
  redisTemplate2()Lorg/springframework/data/redis/core/RedisTemplate;
  @Lorg/springframework/context/annotation/Bean;()
   L0
    LINENUMBER 18 L0
    NEW org/springframework/data/redis/core/RedisTemplate
    DUP
    INVOKESPECIAL org/springframework/data/redis/core/RedisTemplate.<init> ()V
    ARETURN
   L1
    LOCALVARIABLE this Lcom/example/demo/config/RedisConfiguration; L0 L1 0
    MAXSTACK = 2
    MAXLOCALS = 1
}
실제로 앞서 보여드렸던 RedisConfiguration의 바이트 코드를 확인한 결과, 각 메서드의 반환 타입에서 사용한 타입 매개변수들에 대한 정보가 주석에 작성되어 있었습니다.
이렇게 바이트 코드에 제네릭 타입 매개변수에 대한 정보가 명시되어 있으므로 런타임 시점에 해당 타입 매개변수를 참조할 수 있습니다.