IoC 컨테이너 (2) : @Autowired, 컴포넌트 스캔, 빈의 스코프

5. @Autowired

필요한 의존 객체의 “타입"에 해당하는 빈을 찾아 주입한다.

@Autowired

  • required: 기본값은 true (따라서 못 찾으면 애플리케이션 구동 실패)

사용할 수 있는 위치

  • 생성자 (스프링 4.3 부터는 생략 가능)
  • Setter
  • 필드

생성자나 필드에 의존성을 주입받으려고 하는 경우 객체 자체는 만들 수 있지만 @Autowired라는 애노테이션이 붙으면 빈이 있어야만 의존성을 주입받을 수 있기 때문에 애플리케이션 구동에 실패한다. 이 경우에는 @Autowired(required=false)로 지정하면 된다.

public class BookRepository {
}
@Service
public class BookService {
    BookRepository bookRepository;
    
    @Autowired(required = False)
    public void setBookRepository(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }
}

경우의 수

  • 해당 타입의 빈이 없는 경우
  • 해당 타입의 빈이 한 개인 경우
  • 해당 타입의 빈이 여러 개인 경우
    - 빈 이름으로 시도,
    1. 같은 이름의 빈 찾으면 해당 빈 사용
    2. 같은 이름 못 찾으면 실패

같은 타입의 빈이 여러개 일 때

  • @Primary
  • 해당 타입의 빈 모두 주입 받기
  • @Qualifier (빈 이름으로 주입)
  1. @Primary
@Repository @Primary
public class MyBookRepository implements BookRepository{
}
@Repository 
public class MyBookRepository2 implements BookRepository{
}

@Primary 애노테이션이 붙은 MyBookRepository를 사용하게 된다.

  1. 해당 타입의 빈 모두 주입 받기
@Service
public class BookService {
    @Autowired
    List<BookRepository> bookRepositories;
    
        public void printBookRepository(
            this.bookRepositories.forEach(System.out::println);
    }
}
  1. @Qualifier (빈 이름으로 주입)
@Service
public class BookService {
    @Autowired @Qualifier("myBookRepository")
    BookRepository BookRepository;
    
     public void printBookRepository(){
        System.out.println(bookRepository.getClass());
    }
}

또는 필드 이름을 보고 필드 이름과 동일한 타입과 이름을 가진 빈을 주입받는다.

@Service
public class BookService {
    @Autowired 
    BookRepository myBookRepository;
    
     public void printBookRepository(){
        System.out.println(mybookRepository.getClass());
    }
}

동작원리

BeanPostProcessor라는 빈 라이프 사이클 인터페이스에 의해 처리된다. 빈의 라이프사이클에는 빈의 인스턴스를 만든 다음에 Initialization 과정(빈의 이 있는데 그 전후에 BeanPostProcessor 라이프 사이클 인터페이스를 통해 새로 만든 빈 인스턴스를 수정하는 라이프 사이클 콜백을 처리할 수 있다.

@Service
public class BookService {
    @Autowired @Qualifier("myBookRepository")
    BookRepository myBookRepository;

    @PostConstruct
    public void setUp(){
        System.out.println(myBookRepository.getClass());
    }
}
@Service
public class BookService implements InitializingBean {
    @Autowired @Qualifier("myBookRepository")
    BookRepository myBookRepository;

    @Override
    public void afterPropertiesSet() throws Exception {
    }
}

빈 팩토리 라이프사이클 인터페이스와 메소드

https://velog.io/@jsj3282/IoC-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EC%99%80-ApplicationContext-BeanFactory

또한 스프링에서는 AutowiredAnnotationBeanPostProcessor extends BeanPostProcessor를 사용하여 스프링에 제공하는 @Autowired와 @Value, @Inject 애노테이션을 처리할 수 있다. (BeanPostProcessors의 postProcessBeforeInitialization 메소드 부분에서 처리)

확인하는 방법

@Component
public class MyRunner implements ApplicationRunner {

    @Autowired
    ApplicationContext applicationContext;

    /*
    @Autowired
   AutowiredAnnotationBeanPostProcessor processor;
    */
    @Override
    public void run(ApplicationArguments args) throws Exception {
        AutowiredAnnotationBeanPostProcessor bean = applicationContext.getBean(AutowiredAnnotationBeanPostProcessor.class);
        System.out.println(bean);
    }
}

6. @Component와 컴포넌트 스캔

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {

	/**
	 * Alias for {@link #basePackages}.
	 * <p>Allows for more concise annotation declarations if no other attributes
	 * are needed &mdash; for example, {@code @ComponentScan("org.my.pkg")}
	 * instead of {@code @ComponentScan(basePackages = "org.my.pkg")}.
	 */
	@AliasFor("basePackages")
	String[] value() default {};

	/**
	 * Base packages to scan for annotated components.
	 * <p>{@link #value} is an alias for (and mutually exclusive with) this
	 * attribute.
	 * <p>Use {@link #basePackageClasses} for a type-safe alternative to
	 * String-based package names.
	 */
	@AliasFor("value")
	String[] basePackages() default {};

	/**
	 * Type-safe alternative to {@link #basePackages} for specifying the packages
	 * to scan for annotated components. The package of each class specified will be scanned.
	 * <p>Consider creating a special no-op marker class or interface in each package
	 * that serves no purpose other than being referenced by this attribute.
	 */
	Class<?>[] basePackageClasses() default {};

컨포넌트 스캔 주요 기능

  • 스캔 위치 설정
  • 필터: 어떤 애노테이션을 스캔 할지 또는 하지 않을지

@Component

  • @Repository
  • @Service
  • @Controller
  • @Configuration

@Component 애노테이션이 붙은 빈들은 기본적으로 Singleton 스코프이기 때문에 런타임시에 모두 빈을 생성한다. 따라서 런타임시에 시간이 오래 걸릴 수 있다.

스프링 런타임 시간을 짧게 하고 싶은 경우 -> 스프링 5부터 도입된 펑션을 사용한 빈 등록(reflection이나 cglib같은 프록시를 사용하지 않기 때문)

public static void main(String[] args) { 
    new SpringApplicationBuilder() 
        .sources(Demospring51Application.class) 
        .initializers((ApplicationContextInitializer<GenericApplicationContext>) applicationContext -> { 
            applicationContext.registerBean(MyBean.class); 
        }) 
        .run(args); 
 }
@SpringBootApplication
public class Springtest2Application {

    public static void main(String[] args) {
//        SpringApplication.run(Springtest2Application.class, args);
        var app = new SpringApplication(Springtest2Application.class);
        
        app.addInitializers(new ApplicationContextInitializer<GenericApplicationContext>() {
            @Override
            public void initialize(GenericApplicationContext ctx) {
                ctx.registerBean(MyService.class); // 컴포넌트 스캔 밖에 있는 클래스 빈으로 등록
            }
        });
    }
}
@SpringBootApplication
public class Springtest2Application {

   @Autowired
   MyService myService;
    
    public static void main(String[] args) {
//        SpringApplication.run(Springtest2Application.class, args);
        var app = new SpringApplication(Springtest2Application.class);
        
        app.addInitializers((ApplicationContextInitializer<GenericApplicationContext>) ctx -> {
            ctx.registerBean(MyService.class); // 컴포넌트 스캔 밖에 있는 클래스 빈으로 등록
    }
}

또는 Functional Interface를 사용할 수도 있다.

@SpringBootApplication
public class Springtest2Application {

    @Autowired
    MyService myService;

    public static void main(String[] args) {
//        SpringApplication.run(Springtest2Application.class, args);
        var app = new SpringApplication(Springtest2Application.class);
        /*
        app.addInitializers(new ApplicationContextInitializer<GenericApplicationContext>() {
            @Override
            public void initialize(GenericApplicationContext ctx) {
                ctx.registerBean(MyService.class);
            }
        });
        */

        app.addInitializers((ApplicationContextInitializer<GenericApplicationContext>) ctx -> {
            ctx.registerBean(MyService.class);
            ctx.registerBean(ApplicationRunner.class, () -> args1 -> System.out.println("Functional Bean Definition!!"));
//            ctx.registerBean(ApplicationRunner.class, new Supplier<ApplicationRunner>() {
//                @Override
//                public ApplicationRunner get() {
//                    return args1 -> System.out.println("Functinal Bean Definition!!");
//                }
//                @Override
//                public ApplicationRunner get() {
//                    return new ApplicationRunner() {
//                        @Override
//                        public void run(ApplicationArguments args) throws Exception {
//                            System.out.println("Functional Bean Definiton!!");
//                        }
//                    };
//                }
//            });
        });
        app.run(args);
    }
}

@ComponentScan을 버리고 펑션를 사용해 일일이 빈을 등록하는 것 보다는 @Configuration + @Bean을 사용하는 방법으로 등록된 @Bean을 등록하기 위한 것 정도는 괜찮다.

동작 원리

  • @ComponentScan은 스캔할 패키지와 애노테이션에 대한 정보
  • 실제 스캐닝은 ConfigurationClassPostProcessor라는 BeanFactoryPostProcessor에 의해 처리 됨.

BeanFactoryPostProcessor는 다른 모든 빈들을 만들기 이전에 적용된다. 즉, 다른 모든 빈들을 만들기 이전에 컴포넌트 스캔을 해서 적용한다는 것이다.

7. 빈의 스코프

스코프

  • 싱글톤(Default)
  • 프로토타입
    - Request
    - Session
    - WebSocket
    - ...

프로토타입 빈이 싱글톤 빈을 참조하면?

  • 아무 문제 없음.
@Component @Scope(value = "prototype")
public class Proto {

    @Autowired
    Single single;
}

싱글톤 빈이 프로토타입 빈을 참조하면?

@Component
public class Single {

    @Autowired
    private Proto proto;

    public Proto getProto() {
        return proto;
    }
}
  • 프로토타입 빈이 업데이트가 안되네?
  • 업데이트 하려면
    - scoped-proxy
    - Object-Provider
    - Provider (표준)
@Component @Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS) 
public class Proto {

    @Autowired
    Single single;
}

cglibrary라는 third-party tool을 사용해서 class 기반의 proxy를 만들도록 한다. 원래 자바에서는 인터페이스 기반의 proxy 밖에 만들 수 없다. proxy 또한 같은 타입이기 때문에 주입받을 수 있다.

또한 proxy를 사용하는 것 말고도 다른 방법이 있다.

@Component
public class Single {

    @Autowired
    private ObjectProvider<Proto> proto;

    public Proto getProto() {
        return proto.getIfAvailable();
    }
}

싱글톤 객체 사용시 주의할 점

  • 프로퍼티가 공유.
  • ApplicationContext 초기 구동시 인스턴스 생성.
@Component
public class Single {

    @Autowired
    private Proto proto;

    // 쓰레드 세이프하다고 보장받을 수 없음(공유되는 bean)
    int value = 0;
    
    public Proto getProto() {
        return proto;
    }
}

참고

좋은 웹페이지 즐겨찾기