본문 바로가기

SpringFramework Core - I. IoC 컨테이너/9. 어노테이션 기반의 컨테이너 설정

9.4. Qualifier들을 활용한 어노테이션 기반의 정교한 Autowiring

원문: https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans-autowired-annotation-qualifiers

 

 

@Primary는 몇몇 인스턴스들에서 하나의 최우선자가 선택될 수 있도록 하는, 효과적인 타입 autowiring 방법이다. 만약 선택 과정에 좀 더 제어가 필요하다면 스프링의 @Qualifier 어노테이션을 사용할 수 있다. 수식하는 값들을 특정 인자들에 부여하면, 매치되는 타입의 집합이 줄어들어 특정 bean이 각 인자들을 위해 선택된다. 가장 단순한 예시에서는 이것이 평범한 서술적 값을 갖는다. 다음 예시를 보자.

public class MovieRecommender {

    @Autowired
    @Qualifier("main")
    private MovieCatalog movieCatalog;
    
    // ...
}

@Qualifier 어노테이션으로 개별 생성자의 인자들이나 메소드의 매개변수들을 특정해줄 수도 있다. 다음 예시를 보자.

public class MovieRecommender {

    private MovieCatalog movieCatalog;
    
    private CustomerPreferenceDao customerPreferenceDao;
    
    @Autowired
    public void prepare(@Qualifier("main") MovieCatalog movieCatalog,
                       CustomerPreferenceDao customerPreferenceDao) {
        this.movieCatalog = movieCatalog;
        this.customerPreferenceDao = customerPreferenceDao;
    }
    
    // ...
}

다음 예시는 해당하는 bean 정의들이다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">
        
    <context:annotation-config />
    
    <bean class="example.SimpleMovieCatalog">
        <qualifier value="main" />
        
        <!-- 이 bean이 필요로 하는 의존성들을 주입한다 -->
    </bean>
    
    <bean class="example.SimpleMovieCatalog">
        <qualifier value="action" />
        
        <!-- 이 bean이 필요로 하는 의존성들을 주입한다 -->
    </bean>
    
    <bean id="movieRecommender" class="example.MovieRecommender" />
    
</beans>

'main'이나 'action' 수식값을 가진 bean은 일치하는 값을 가진 생성자 인자와 wire된다.

 

매칭 실패에 대비하여, bean의 이름은 수식의 기본값으로 간주된다. 그래서 'main'이라는 id를 수식자 요소 대신에 설정하면 같은 매칭 결과를 얻을 수 있다. 그러나 특정 bean들을 이름을 통해 참조하는 이런 관습을 활용할 수 있음에도 불구하고, @Autowired는 선택적인 수식자들에 비해 좀 더 근본적인 type-driven 주입이다. 즉 수식값들은 bean 이름이라는 대비책을 가지고 있음에도 불구하고 매칭되는 타입의 집합을 줄이는 의미만을 가질 뿐이다. 수식값들은 그 의미로써 유일한 bean id에 대한 참조를 표현하지 않는다. 좋은 수식값들은 'main', 'EMEA' 또는 'persistent'와 같은 것들로서, 이들은 bean의 id로부터 독립된 특정 컴포넌트의 특징들을 표현해준다. 위 예시에서 볼 수 있었던 것처럼 익명의 bean 정의의 경우에는 자동으로 생성될 수도 있는 것이다.

 

수식자들은 이미 논의한 바 있는 Set<MovieCatalog>에서처럼 타입이 정해진 컬렉션에도 적용될 수 있다. 이 경우에는 선언된 수식자들에 따라 매칭되는 모든 bean들이 컬렉션으로 주입된다. 이는 수식자들이 유일해야할 필요가 없다는 것을 암시한다. 대신 그들은 필터의 기준으로써 여겨진다. 예를 들어, 'action'이라는 수식자를 똑같이 갖고 있는 여러 개의 MovieCatalog를 정의할 수 있다. 이 여러개의 MovieCatalog들은 @Qualifier("action")이라는 어노테이션이 붙은 Set<MovieCatalog>로 모두 주입된다.

타입 기반 매칭에서 수식값들이 대상 bean의 이름을 취하면 주입 지점에 @Qualifier 어노테이션이 필요게 된다. 만약 수식자나 최우선자 표시와 같이 분석 지시자를 따로 가지고 있지 않다면, 의존성이 유일하지 않은 상황에서는 스프링이 주입 지점의 이름과 대상 bean의 이름을 비교하고 같은 이름의 후보를 선택하게 된다.

여러분이 이름을 통해 어노테이션 기반의 주입을 표현하려고 한다면, 타입이 일치하는 후보들 가운데 이름으로 선택이 가능할지라도 @Autowired를 우선적으로 사용해서는 안된다. 대신, JSR-250의 @Resourse 어노테이션을 사용하라. 이 어노테이션은 매칭 과정과는 무관하게 선언된 타입과 유일한 이름을 바탕으로 대상 컴포넌트를 식별하기 위해서 정의되었다. @Autowired는 그와는 다른 의미를 지닌다. 타입을 통해 후보 bean들을 선택한 후, 그 후보들 가운데서만 문자열의 수식값이 고려된다는 것이다.

 

컬렉션, 맵, 배열로 정의된 bean들에게, @Resource는 좋은 해결책이 될 수 있다. 유일한 이름을 통해 특정 컬렉션이나 배열 bean을 참조하기 때문이다. 컬렉션 4.3에서부터 맵과 배열 또한 @Autowired의 타입 매칭 알고리즘을 통해 매치시킬 수 있다. 요소의 타입 정보가 @Bean의 반환 타입이나 컬렉션 상속 계층에 보존만 되어 있다면 말이다. 이런 경우에는 같은 수식값을 사용해서 타입의 컬렉션들 사이에서 선택을 할 수 있다.

 

4.3에서부터, @Autowired를 통해 주입을 위한 자기 참조가 가능하다(즉, 현재 주입당하고 있는 bean을 참조하는 것이다). 자기 주입은 대비책이라는 점을 주목하자. 보통의 의존성들이 우선권을 가지고 있다. 그런 점에서 자기 참조는 일반적인 후보 선택과정에 참여하지 않으며, 따라서 우선할 수 없게 된다. 반대로 그들은 가장 낮은 우선순위로써 끝을 맺는다. 실제로, 자기 참조는 가장 마지막 의존성에만 사용해야한다(예를 들어, bean의 트랜잭션 프록시를 통해 같은 인스턴스의 다른 메소드들을 호출하는 경우가 해당된다). 그런 경우에는 영향을 받는 메소드들을 분리된 위임 bean으로 추출하는 것을 고려하라. 또는 대안으로써, @Resource를 사용할 수 있다. 유일한 이름을 통해 현재의 bean으로 프록시를 다시 돌려주기 때문이다.

 

※ 같은 설정 클래스의 메소드로 @Bean의 결과들을 주입하는 것 또한 효과적인 자기 참조이다. 메소드 안에서 그런 참조들을 늦게 분석하게 하거나, 영향을 받는 @Bean 메소드들을 정적으로 선언하라. 그 참조들을 해당 인스턴스나 그 라이프사이클과 decoupling시킬 수 있기 때문이다. 그렇지 않으면 bean들은 fallback 단계에서, 최우선자 후보로 뽑힌 다른 설정 클래스들의 bean과 매치되어버릴 것이다.

 

@Autowired는 필드, 생성자, 복수의 매개변수를 가진 메소드에 적용된다. 그와 동시에 매개변수 단위에서 수식자 어노테이션을 통해 범위를 좁힌다. 그와 반대로, @Resource는 필드와 bean 프로퍼티 setter 메소드에만 단일 매개변수로 적용된다. 결과적으로 주입 대상이 생성자이거나 복수의 매개변수를 가진 메소드라면 수식자들을 통해 확정가능하도록 하는 것이 좋다.

 

커스텀한 수식자 어노테이션을 만들 수도 있다. 그렇게 하기 위해서는, 다음 예시처럼 어노테이션을 정의하고 @Qualifier 어노테이션을 여러분의 정의 안에 제공하면 된다. 

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @Interface Genre {

    String value();
}

그리고나서 여러분은 autowired된 필드들과 매개변수들에 커스텀한 수식자를 제공할 수 있게 된다. 다음 예시를 보자.

 

public class MovieRecommender {

    @Autowired
    @Genre("Action")
    private MovieCatalog actionCatalog;
    
    private MovieCatalog comedyCatalog;
    
    @Autowired
    public void setComedyCatalog(@Genre("Comedy") MovieCatalog comedyCatalog) {
        this.comedyCatalog = comedyCatalog;
    }
    
    // ...
}

그 다음으로, 후보인 bean 정의들에게 정보를 제공한다. <bean/> 태그의 하위 요소로 <qualifier/>태그를 제공하고 type과 value를 여러분의 커스텀 수식자 어노테이션에 맞게 특정해주면 된다. 타입은 어노테이션의 fully-qualified한 클래스명으로 써주면 된다. 만약 이름이 충돌할 위험이 없다면 더 편하게 하기 위해서 짧은 클래스명을 써도 된다. 다음 예시는 그 두 접근법을 모두 보여준다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">
        
    <context:annotation-config />
    
    <bean class="example.SimpleMovieCatalog">
        <qualifier type="Genre" value="Action" />
        <!-- 이 bean에 필요한 의존성들을 주입한다 -->
    </bean>
    
    <bean class="example.SimpleMovieCatalog">
        <qualifier type="example.Genre" value="Comedy"/>
        <!-- 이 bean에 필요한 의존성들을 주입한다 -->
    </bean>
    
    <bean id="movieRecommender" class="example.MovieRecommender" />
    
</beans>

클래스패스 스캐닝과 관리되는 컴포넌트들에서는, XML에 있는 수식자 관련 메타데이터를 어노테이션 기반으로 대신 제공하는 것을 보게될 것이다. 특히, '수식자 메타데이터를 어노테이션으로 제공하기'를 참고하라.

 

몇몇 경우에는, 값 없이 어노테이션을 사용하는 것만으로도 충분하다. 이는 어노테이션이 좀 더 포괄적인 역할을 수행하면서, 다른 타입의 의존성들에 적용될 때 유용하다. 예를 들어, 인터넷 연결이 없을 때 오프라인 카탈로그를 제공하게 될 수도 있다. 그렇다면, 먼저 다음 예시와 같이 간단한 어노테이션을 정의하라.

@Target({ElementType.FILED, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Offline {

}

그리고나서 어노테이션을 autowired될 필드나 프로퍼티에 붙인다. 다음 예시를 보자.

public class MovieRecommender {

    @Autowired
    @Offline
    private MovieCatalog offlineCatalog;
    
    // ...
}

이제 bean 정의는 단지 수식자의 type만을 필요로하게 된다. 다음 예시를 보자.

<bean class="example.SimpleMovieCatalog">
    <qualifier type="Offline" />
    <!-- 이 bean에 필요한 의존성들을 주입한다 -->
</bean.

또한 여러분은 기본적인 value 속성에 추가되거나 이를 대체하는 이름 속성들을 가진 커스텀한 수식자 어노테이션들을 정의할 수도 있다. 그리고나서, 만약 복수의 속성값들이 필드나 매개변수에 특정되면, bean 정의는 모든 속성값들을 autowire 후보들과 매치하는지 고려하게 된다. 다음 예시의 어노테이션 정의를 주목하자.

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface MovieQualifier {

    String genre();
    
    Format format();
    
}

이 경우에 'Format'은 다음 예시와 같이 enum이다.

public enum Format {
    VHS, DVD, BLUERAY
}

autowired될 필드들은 커스텀한 수식자 어노테이션이 붙어있으며, genre와 format이라는 두 속성값을 모두 포함하고 있다. 다음 예시를 보자.

public class MovieRecommender {

    @Autowired
    @MovieQualifier(format=Format.VHS, genre="Action")
    private MovieCatalog actionVhsCatalog;
    
    @Autowired
    @MovieQualifier(format=Format.VHS, genre="Comedy")
    private MovieCatalog comedyVhsCatalog;
    
    @Autowired
    @MovieQualifier(format=Format.DVD, genre="Action")
    private MovieCatalog actionDvdCatalog;
    
    @Autowired
    @MovieQualifier(format=Format.BLUERAY, genre="Comedy")
    private MovieCatalog comedyBluRayCatalog;
    
    // ...
}

마지막으로, bean 정의들은 일치하는 수식자 값들을 가지고 있어야한다. 이 예시에서도 <qualifier/> 요소들 대신에 bean의 메타 속성들을 사용할 수 있다는 것을 보여주었다. 만약 가능하다면 <qualifier/> 요소와 그 속성들이 우선하겠지만, autowiring 메커니즘은 그런 수식자가 없을때는 <meta/> 태그들 안에서 제공되는 값들을 대비책으로써 활용한다. 다음 두 bean 정의들을 보자.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">
        
    <context:annotation-config />
    
    <bean class="example.SimpleMovieCatalog">
        <qualifier type="MovieQualifier">
            <attribute key="format" value="VHS" />
            <attribute key="genre" value="Action" />
        </qualifier>
        <!-- 이 bean이 필요로하는 의존성을 주입한다 -->
    </bean>
    
    <bean class="example.SimpleMovieCatalog">
        <meta key="format" value="DVD" />
        <meta key="genre" value="Action" />
        <!-- 이 bean이 필요로하는 의존성을 주입한다 -->
    </bean>
    
    <bean class="example.SimpleMovieCatalog">
        <meta key="format" value="BLUERAY" />
        <meta key="genre" value="Comedy" />
        <!-- 이 bean이 필요로하는 의존성을 주입한다 -->
    </bean>
    
</beans>