Spring Framework

[Spring Core] Autowiring (@Autowired, @Qualifier, @Primary, @Resource)

kgvovc 2021. 4. 3. 15:10
반응형

Autowiring

Autowiring자바 기반 설정 방식에서 @Bean 메서드를 사용하거나 XML 기반 설정 방식에서 <bean> 요소를 사용하는 것처럼 명시적으로 빈을 정의하지 않고도 DI 컨테이너에 빈을 자동으로 주입하는 방식이다.

 

Autowiring에는 타입을 이용한 방식(autowiring by type)과 이름을 사용한 방식(autowiring by name)이 있다. 이제 이 두 방식에 대해 알아보자.

 

 

 

[타입으로 Autowiring하기]

타입으로 Autowiring하는 방식세터 인젝션, 컨스트럭터 인젝션, 필드 인젝션의 세 가지 의존성 주입 방법에서 모두 활용할 수 있다. 타입으로 Autowiring 할 때는 기본적으로 의존성 주입이 반드시 성공한다고 가정한다. 그래서 주입할 타입에 해당하는 빈을 DI 컨테이너가 찾지 못한다면 org.springframework.beans.factory.NoSuchBeanDefinitionException이라는 예외가 발생한다.

 

 

만약 이러한 필수 조건을 완화하고 싶다면 다음과 같이 @Autowired 애너테이션의 required 속성에 false를 설정하면 된다. 해당 타입의 빈을 찾지 못하더라도 예외가 발생하지 않고 의존성 주입은 실패했기 때문에 해당 필드의 값은 null이 된다.

 

  • Autowiring의 필수 조건을 완화해서 필드 인젝션을 한 예(required = false)
@Component
public class UserServiceImpl implements UserService {
    
    @Autowired(required = false)
    PasswordEncoder passwordEncoder;
    
    // 생략
}

 

 

 

 

스프링 프레임워크 4부터는 필수 조건을 완화할 때 required = false를 사용하는 대신 Java SE 8부터 도입된 java.util.Optional을 사용할 수 있다.

 

  • Autowiring의 필수 조건을 완화해서 필드 인젝션을 한 예(Optional)
@Autowired
Optional<PasswordEncoder> passwordEncoder;

public void createUser(User user, String rawPassword) {
    String encodedPassword = passwordEncoder.map(x -> x.encode(rawPassword)).orElse(rawPassword);
    
    // ...
}

 

 

 

@Qualifier 애너테이션

한편 타입으로 Autowiring을 할 때 DI 컨테이너에 같은 타입의 빈이 여러 개 발견된다면 그 중에서 어느 것을 사용해야 할지 알 수가 없다. 그래서 이런 경우에는 NoUniqueBeanDefinitionException 예외(org.springframework.beans.factory.NoUniqueBeanDefinitionException)가 발생한다. 이처럼 같은 타입의 빈이 여러 개 정의된 경우에는 @Qualifier 애너테이션(org.springframework.beans.factory.annotation.Qualifier)을 추가하면서 빈 이름을 지정하면 같은 타입의 빈 중에서 원하는 빈만 선택할 수 있다.

 

 

 

다음은 같은 타입의 빈 여러 개자바 기반 설정 방식으로 정의된 경우다.

 

  • 두 개의 PasswordEncoder를 자바 기반 설정 방식으로 정의한 예
@Configuration
@ComponentScan
public class AppConfig {
    @Bean
    PasswordEncoder sha256PasswordEncoder() {
        return new Sha256PasswordEncoder();
    }
    
    @Bean
    PasswordEncoder bcryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    // 생략
}

 

이 예에서는 PasswordEncoder 인터페이스를 구현한 클래스가 두 개 있는데 하나는 SHA-256 방식, 또 다른 하나는 BCrypt 방식의 알고리즘으로 구현돼 있다. 이것들은 같은 인터페이스를 구현하고 있기 때문에 @Autowired만으로는 빈을 구분하지 못한다. 그래서 빈의 이름을 추가로 명시할 필요가 있다. 만약 SHA-256을 사용하는 경우는 다음과 같이 @Qualifier 애너테이션을 추가하고 sha256PasswordEncoder라는 이름을 명시하면 된다.

 

 

  • @Qualifier를 사용해 빈 이름 명시
@Component
public class UserServiceImpl implements UserService {
    @Autowired
    @Qualifier("sha256PasswordEncoder")
    PasswordEncoder passwordEncoder;
    // 생략
}

 

 

 

@Primary 애너테이션

한편 자바 기반 설정 방식에서 @Primary 애너테이션 (org.springframework.context.annotation.Primary)을 사용하면 @Qualifier를 사용하지 않았을 때 우선적으로 선택될 빈을 지정할 수 있다.

 

  • @Primary를 사용해 기본 빈을 지정
@Configuration
@ComponentScan
public class AppConfig {
    @Bean
    PasswordEncoder sha256PasswordEncoder() {
        return new Sha256PasswordEncoder();
    }
    
    @Bean
    @Primary
    PasswordEncoder bcryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    // 생략
}

 

 

위 같이 설정된 상태에서 다음과 같이 @Qualifier를 따로 지정하지 않는 경우 @Primary 애너테이션이 붙은 bcryptPasswordEncoder가 사용된다.

 

 

  • @Primary로 지정된 빈이 선택되도록 PasswordEncoder를 사용하는 예
@Autowired
PasswordEncoder passwordEncoder;

 

 

 

만약 자바 기반 설정을 변경하지 않고 sha256PasswordEncoder로 교체하려면 @Autowired@Qualifier를 추가하고 명시적으로 sha256PasswordEncoder라는 이름을 지정하면 된다.

 

 

다만 @Qualifier로 수식하는 빈의 이름에 구현 클래스의 이름이 포함된다거나 구현과 관련된 정보가 포함돼 있다면 그 빈의 명명 방법이 바람직하다고는 볼 수 없다. 왜냐하면 결합도를 낮추기 위해 기껏 DI 방식을 채택했는데, 빈을 사용할 때 특정 구현체가 사용될 것으로 의식한 이름을 지정해 버리면 DI를 사용하는 의미가 없어진다. 이런 경우라면 DI를 아예 사용하지 않는 것이 나을 수 있다.

 

 

그렇다면 어떻게 명명하는 것이 좋을까? 이런 경우에는 빈의 이름으로 구현체의 이름을 쓰는 대신 역할이나 사용 목적, 혹은 용도를 이름으로 쓰는 것이 좋다. 앞의 예에서 같은 타입의 구현체를 여러 개 준비한 이유가 '기본적으로는 보안이 강력한 BCrypt를 제공하지만, 경우에 따라서는 비교적 경량인 SHA-256도 쓸 수 있게 하고 싶다'라는 요구사항을 반영한 것이라고 볼 수 있다. 그렇다면 Sha256PasswordEncoder의 빈 이름을 요구사항의 취지와 목적에 맞춰 'lightweight'라고 지을 수 있을 것이다.

 

 

 

  • SHA-256으로 구현한 PasswordEncoder의 이름을 용도에 맞게 지정한 예
@Configuration
@ComponentScan
public class AppConfig {
    
    @Bean(name = "lightweight")
    PasswordEncoder sha256PasswordEncoder() {
        return new Sha256PasswordEncoder();
    }
    
    @Bean
    @Primary
    PasswordEncoder bcryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

 

 

만약 패스워드를 다룰 때 처리 속도가 빠르고 부담이 적은 경량 알고리즘을 사용하고 싶다면 다음과 같이 @Qualifier를 설정하면 된다.

 

  • 빈 이름이 lightweight인 PasswordEncoder를 사용하는 예
@Autowired
@Qualifier("lightweight")
PasswordEncoder passwordEncoder;

 

 

 

 

애너테이션을 직접 정의하기

한편 빈의 역할이나 용도는 문자열 형태의 이름이 아닌 타입(애너테이션)으로 표현할 수도 있다. 다음은 @Lightweight 애너테이션을 직접 만들고 @Qualifier 역할을 하도록 만든 예다.

 

  • @Qualifier 역할을 한 @Lightweight 애너테이션의 구현 예
import org.springframework.beans.factory.annotation.Qualifier;
import java.lang.annotation.*;

@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface Lightweight {
    
}

 

 

이제 자바 기반 설정 방식에서 경량 알고리즘을 사용하기 위해 @Lightweight 애너테이션을 활용해 보자.

 

  • 직접 작성한 @Lightweight 애너테이션을 활용해 빈을 정의한 예
@Configuration
@ComponentScan
public class AppConfig {
    
    @Bean
    @Lightweight
    PasswordEncoder sha256PasswordEncoder() {
        return new Sha256PasswordEncoder();
    }
    
    @Bean
    @Primary
    PasswordEncoder bcryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    // 생략
}

 

 

이 같이 정의되면 필드 인젝션을 할 때도 @Lightweight를 활용할 수 있다.

 

  • 직접 작성한 @Lightweight 애너테이션을 활용해 필드 인젝션을 하는 예
@Autowired
@Lightweight
PasswordEncoder passwordEncoder;

 

이처럼 직접 애너테이션을 정의하는 방식은 문자열로 빈 이름을 지정하는 방식과 달리 오타가 발생하더라도 사전에 확인할 수 있고, 여러 개의 유사한 빈을 정의해야 한다면 최적의 방법이라 생각할 수 있다. 물론 앞서 설명한 것처럼 @Sha256과 같이 구현 정보가 직접 노출되는 애너테이션 이름은 피하는 것이 좋다.

 

 

 

 

 

[이름으로 Autowiring하기]

 

@Resource 애너테이션

한편 빈의 이름필드명이나 프로퍼티명과 일치할 경우에 빈 이름으로 필드 인젝션을 하는 방법도 있다. 이 방법에서는 JSR 250 사양을 지원하는 @Resource 애너테이션(javax.annotation.Resource)을 활용한다.

 

다음은 앞에서 설명한 @Qualifier의 예를 @Resource로 대체한 것이다.

 

  • @Resource 애너테이션을 활용해 필드 인젝션을 하는 예
@Component
public class UserServiceImpl implements UserService {
    @Resource(name = "sha256PasswordEncoder")
    PasswordEncoder passwordEncoder;
    // 생략
}

 

 

이때 @Resource 애너테이션의 name 속성을 생략할 수 있는데, 필드 인젝션을 하는 경우에는 필드 이름과 같은 이름의 빈이 선택되고, 세터 인젝션을 하는 경우에는 프로퍼티 이름과 같은 이름의 빈이 선택된다.

 

  • @Resource 애너테이션을 활용해 필드 인젝션을 하는 예(필드 이름과 일치)
@Component
public class UserServiceImpl implements UserService {
	@Resource
    PasswordEncoder sha256PasswordEncoder;
    // 생략
}

 

 

  • @Resource 애너테이션을 활용해 세터 인젝션을 하는 예(프로퍼티 이름과 일치)
@Component
public class UserServiceImpl implements UserService {
    private PasswordEncoder passwordEncoder;
    
    @Resource
    public void setSha256PasswordEncoder(PasswordEncoder passwordEncoder) {
        this.passwordEncoder = passwordEncoder;
    }
}

 

만약 위의 어느 경우에도 해당되지 않으면 타입으로 Autowiring을 시도한다. @Resource 동작 방식은 조금 복잡한 편이라서 동작 방식을 제대로 이해한 후에 사용할 것을 권장한다. 참고로 컨스트럭터 인젝션에서는 @Resource 애너테이션을 사용하지 못한다.

 

 

 

 

 

[컬렉션이나 맵 타입으로 Autowiring하기]

지금까지 같은 인터페이스를 구현하는 빈이 여러 개 정의된 경우에 @Qualifier@Resource를 활용해 인젝션할 대상을 한정하는 방법을 살펴봤다. 스프링 프레임워크에서는 이렇게 단 하나의 빈만 가져오는 방법 외에도 같은 인터페이스를 구현한 빈을 컬렉션(Collection)이나 맵(Map) 타입에 담아서 가져오는 방법도 제공한다.

 

 

  • IF 인터페이스를 구현한 빈을 여러 개 정의한 예(인터페이스)
public interface IF<T> {
}

@Component
public class IntIF1 implements IF<Integer> {
}

@Component
public class IntIF2 implements IF<Integer> {
}

@Component
public class StringIF implements IF<String> {
}

 

 

이처럼 같은 인터페이스를 구현한 빈이 여러 개 있는 경우 Autowiring을 다음과 같은 형태로 할 수도 있다.

 

  • IF 인터페이스를 구현한 빈을 모두 가져오기
@Autowired
List<IF> ifList;

@Autowired
Map<String, IF> ifMAp;

 

ifList에는 IntIF1, IntIF2, StringIF와 같은 빈이 리스트 형태로 주입된다. 그리고 ifMap에는 '빈 이름 = 빈'과 같은 형식으로 {intIF1 = IntIF1 빈, intIF2 = IntIF2 빈, stringIF = StringIF 빈}이 맵 형태로 주입된다.

 

 

 

이제 제네릭(Generic)의 타입 파라미터에 구체적인 값을 넣어 보자.

 

  • IF<Integer> 인터페이스를 구현한 빈을 모두 가져오기
@Autowired
List<IF<Integer>> ifList;

@Autowired
Map<String, IF<Integer>> ifMap;

 

이렇게 하면 주입될 빈의 타입 파라미터가 Integer로 한정되기 때문에 ifList에는 IntIF1, IntIF2와 같은 빈만 주입된다. 그리고 ifMap에는 {intIF1 = IntIF1 빈, intIF2 = IntIF2 빈}만 주입된다.

 

 

 

그렇다면 애당초 처음부터 리스트나 맵 형태로 빈을 정의해보면 어떨까? 다음 예를 살펴보자.

 

  • 리스트와 맵 형태로 빈을 정의
@Bean
List<IF> ifList() {
	return Arrays.asList(new IntIF1(), new IntIF2(), new StringIF());
}

@Bean
Map<String, IF> ifMap() {
    Map<String, IF> map = new HashMap<>();
    map.put("intIF1", new IntIF1());
    map.put("intIF2", new IntIF2());
    map.put("stringIF", new StringIF());
    return map;
}

 

사실 이러한 방식으로 빈을 정의한 경우에는 @Autowired 애너테이션을 사용하더라도 실제로는 Autowiring 되지 않는다.

 

  • @Autowired 애너테이션을 이용한 필드 인젝션(인젝션 불가)
@Autowired
@Qualifier("ifList") // 인젝션 불가
List<IF> ifList;

@Autowired
@Qualifier("ifMap") // 인젝션 불가
Map<String, IF> ifMap;

 

 

다만 이런 경우에는 다음과 같이 @Resource를 사용하면 Autowiring 할 수 있다.

 

  • @Resource 애너테이션을 이용한 필드 인젝션(인젝션 가능)
@Resource // 인젝션 가능
List<IF> ifList;

@Resource // 인젝션 가능
Map<String, IF> ifMap;
반응형