Home > Spring > (Spring) - 빈과 컴포넌트 스캔

(Spring) - 빈과 컴포넌트 스캔
spring

Spring MVC 살펴보기, 스프링이 제공하는 ServletContainerInitializer 먼저 읽고 오자.

빈(Bean)

  • 스프링에서 빈은 스프링 컨테이너(=IoC 컨테이너=DI 컨테이너)에 의해 관리되는 객체이다.
    • 스프링 빈은 일반적인 객체와는 달리 스프링 프레임워크의 기능과 규칙에 따라 동작하는 특별한 객체로 볼 수 있다.
    • 스프링 컨테이너는 이러한 빈들을 생성 및 관리하고, 필요에 따라 의존성을 주입하여 개발자가 객체의 생명주기와 관련된 복잡한 작업을 처리하도록 도와준다.

  • 특징
    • 스프링 빈은 개발자가 직접 생성하고 관리하는 것이 아니라, 스프링 컨테이너에 의해 생성되고 관리된다. 개발자는 빈의 설정을 정의하고, 스프링 컨테이너는 이를 바탕으로 빈을 생성하고 관리한다.
    • 스프링 빈은 의존 객체를 직접 생성하고 주입할 필요 없이 의존성 주입(DI)을 통해 다른 빈들과의 관계를 설정할 수 있다. 의존성 주입은 xml 설정, 자바 설정(@Configuration과 @Bean), @Autowired 어노테이션 등을 통해 이루어진다.
    • 스프링 빈은 기본적으로 Singleton 스코프를 가지며, 다양한 스코프(Prototype, Request, Session 등)를 설정할 수 있다. 스코프는 빈을 생성하기 위한 방법을 말하며, Singleton 스코프는 스프링 컨테이너당 하나의 인스턴스만 존재함을 의미한다.

  • 스프링 빈 등록 방법
    • xml 설정
    • 자바 설정(@Configuration과 @Bean)
    • 어노테이션(@Component, @Service, @Controller 등) - 컴포넌트 스캔

  • 자바 설정 간단한 예시
      public class MemoryMemberRepository implements MemberRepository 
      {
          private static Map<Long, Member> store = new HashMap<>();
    
          @Override
          public void save(Member member) 
          { store.put(member.getId(), member); }
      }
    
      public class MemberServiceImpl implements MemberService 
      {
          private final MemberRepository memberRepository;
    
          public MemberServiceImpl(MemberRepository memberRepository) 
          { this.memberRepository = memberRepository; }
    
          @Override
          public void join(Member member) 
          { memberRepository.save(member); }
      }
    
      @Configuration
      public class AppConfig 
      {
          @Bean
          public MemberRepository memberRepository() 
          { 
              System.out.println("memberRepository() 호출");
              return new MemoryMemberRepository(); 
          }
    
          @Bean
          public MemberService memberService() 
          { 
              return new MemberServiceImpl(memberRepository()); 
          }
      }
    
      public class Example 
      {
          public static void main(String[] args) 
          {
              ApplicationContext ac 
                  = new AnnotationConfigWebApplicationContext();
              ac.register(AppConfig.class);
              ac.refresh();
    
              MemberService memberService 
                  = ac.getBean("memberService", MemberService.class);
    
              Member member = new Member("A", 20);
              memberService.join(member);
          }
      }
    
    • ApplicationContext를 스프링 컨테이너라고 한다.
      • 스프링 컨테이너는 AppConfig를 설정 정보로 사용하며, 여기서 @Bean이 붙은 모든 메소드를 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라고 한다. (@Bean이 붙은 메소드명 = 스프링 빈의 이름)
      • 스프링 컨테이너는 설정 정보를 참고해서 의존관계를 주입(DI)한다.
  • 설정 정보(AppConfig.class)에서 @Configuration 없이 @Bean만 적용하면 어떻게 될까?
    • @Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다. memberService() 메소드의 memberRepository() 메소드처럼, 의존관계 주입이 필요해서 메소드를 직접 호출할 때 싱글톤을 보장하지 않는다. 쉽게 얘기해서, “memberRepository() 호출” 메시지가 2번 출력될 수 있다는 말이다.
    • 반면, @Configuration을 적용하면 @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환한다. 즉, 싱글톤이 보장된다. 쉽게 얘기해서, “memberRepository() 호출” 메시지는 1번만 출력된다는 말이다.
      -> 스프링 설정 정보는 항상 @Configuration을 사용하자.

컴포넌트 스캔

  • 컴포넌트 스캔은 xml 설정 정보(‘bean’ 태그)나 자바 설정 정보(@Configuration과 @Bean)가 없어도 자동으로 스프링 빈을 등록할 수 있는 기능이다.
    • 컴포넌트 스캔을 사용하려면 설정 정보에 @ComponentScan을 붙여주면 된다.(기존의 자바 설정 정보와는 다르게 @Bean으로 등록한 클래스가 없다.)
    • @ComponentScan은 탐색 위치에 @Component 어노테이션이 붙은 모든 클래스를 스캔해서 스프링 빈으로 등록한다.

  • 기본 스캔 대상
    • 컴포넌트 스캔은 @Component뿐만 아니라 아래의 내용도 추가로 대상에 포함한다.(아래의 내용의 코드들을 보면 @Component를 포함하고 있다.)
      • @Controller : 스프링 MVC 컨트롤러에서 사용
      • @Service : 스프링 비즈니스 로직에서 사용
      • @Repository : 스프링 데이터 접근 계층에서 사용
      • @Configuration : 스프링 설정 정보에서 사용

  • 탐색 위치
    @ComponentScan(basePackages = "hello.core", )
    
    • basePackages : 탐색할 패키지의 시작 위치(src/main/java 이후의 위치)를 지정하며, 해당 패키지를 포함해서 하위 패키지를 모두 탐색한다. 여러 개의 시작 위치를 지정할 수도 있다.
    • basePackageClasses : 지정한 클래스가 속한 패키지를 탐색 시작 위치로 지정한다. 하위 패키지도 모두 탐색한다.
    • 패키지 위치를 지정하지 않으면, @ComponentScan이 붙은 설정 정보 클래스가 속한 패키지가 시작 위치가 된다. 패키지 위치를 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최상단에 두는 방법이 자주 쓰인다.

  • 컴포넌트 스캔 간단한 예시 (위의 자바 설정 예시 수정)
      @Component
      public class MemoryMemberRepository implements MemberRepository 
      { ... }
    
      @Component
      public class MemberServiceImpl implements MemberService 
      {
          private final MemberRepository memberRepository;
    
          @Autowired
          public MemberServiceImpl(MemberRepository memberRepository) 
          { this.memberRepository = memberRepository; }
    
          ...
      }
    
      @Configuration
      @ComponentScan
      public class AppConfig 
      { }
    
      public class Example 
      {
          public static void main(String[] args) 
          {
              ApplicationContext ac 
                  = new AnnotationConfigWebApplicationContext();
              ac.register(AppConfig.class);
              ac.refresh();
    
              MemberService memberService 
                  = ac.getBean("memberService", MemberService.class);
    
              Member member = new Member("A", 20);
              memberService.join(member);
          }
      }
    
    • @Autowired
      • @Autowired는 의존관계를 자동으로 주입해준다.
      • 생성자에 @Autowired를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다. 여러 의존관계도 한번에 주입받을 수 있다.

Spring MVC의 컴포넌트 스캔

  • SpringServletContainerInitializer(WebApplicationInitializer 직접 구현)를 바탕으로 두 가지 예시를 살펴보자.
    1. DispatcherServlet WebApplicationContext만 사용하는 경우
    2. Root WebApplicationContext와 DispatcherServlet WebApplicationContext를 사용하는 경우
  1. 단일 WebApplicationContext의 경우
    • DispatcherConfig.class 작성
        @Configuration
        @ComponentScan
        @EnableWebMvc
        public class DispatcherConfig implements WebMvcConfigurer
        { 
            // 필요한 메소드 Override
      
            // ex1) 뷰 리졸버 구성
            @Override
            public void configureViewResolvers(ViewResolverRegistry registry) {
                InternalResourceViewResolver resolver = new InternalResourceViewResolver();
                resolver.setPrefix("/WEB-INF/views/");
                resolver.setSuffix(".jsp");
                registry.viewResolver(resolver);
            }
      
            // ex2) 메시지 컨버터 구성
            @Override
            public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
                converters.add(new MappingJackson2HttpMessageConverter());
            }
        }
      
    • WebApplicationInitializer 구현
        public class MyWebApplicationInitializer implements WebApplicationInitializer 
        {
            @Override
            public void onStartup(ServletContext servletContext) 
            {
                AnnotationConfigWebApplicationContext dispatcherContext 
                    = new AnnotationConfigWebApplicationContext();
                dispatcherContext.register(DispatcherConfig.class);
                ServletRegistration.Dynamic dispatcher 
                    = servletContext.addServlet("dispatcher", new DispatcherServlet(dispatcherContext));
      
                dispatcher.addMapping("/");
                dispatcher.setLoadOnStartup(1);
            }
        }         
      
      • @EnableWebMvc
        • @EnableWebMvc는 기본적인 Spring MVC 설정을 자동으로 활성화해준다.
          • 예를 들어, 스프링 컨테이너를 생성할 때 입력으로 받는 설정 클래스에는 HandlerMapping, HandlerAdapter 등의 빈이 등록되어 있어야 하는데, 설정 클래스에 @EnableWebMvc를 붙여주면 해당 빈들을 자동으로 추가해주므로 개발자가 직접 빈으로 등록할 필요가 없다.
        • @EnableWebMvc를 사용할 때, WebMvcConfigurer 인터페이스를 구현하여 Spring MVC의 설정을 추가 설정하거나 커스터마이징할 수 있다.
          • 예를 들어, 필요한 메소드를 오버라이드하여 인터셉터 추가, 뷰 리졸버 설정 등을 할 수 있다.
  2. Root WebApplicationContext가 추가된 경우
    • AppConfig.class 작성
        @Configuration
        @ComponentScan(excludeFilters={@Filter(org.springframework.stereotype.Controller.class)})
        public class AppConfig
        { }
      
    • DispatcherConfig.class 작성
        @Configuration
        @ComponentScan(basePackageClasses = AppConfig.class, useDefaultFilters=false, includeFilters={@Filter(org.springframework.stereotype.Controller.class)})
        @EnableWebMvc
        public class DispatcherConfig implements WebMvcConfigurer
        { }
      
    • WebApplicationInitializer 구현
        public class MyWebAppInitializer implements WebApplicationInitializer 
        {
            @Override
            public void onStartup(ServletContext container) 
            {
                // Root WebApplicationContext
                AnnotationConfigWebApplicationContext rootContext 
                    = new AnnotationConfigWebApplicationContext();
                rootContext.register(AppConfig.class);
                container.addListener(new ContextLoaderListener(rootContext));
      
                // DispatcherServlet의 WebApplicationContext
                AnnotationConfigWebApplicationContext dispatcherContext 
                    = new AnnotationConfigWebApplicationContext();
                dispatcherContext.register(DispatcherConfig.class);
                ServletRegistration.Dynamic dispatcher 
                    = container.addServlet("dispatcher", new DispatcherServlet(dispatcherContext));
      
                dispatcher.addMapping("/"); 
                dispatcher.setLoadOnStartup(1); 
            }
        }
      
      • 컴포넌트 스캔 시점
        • Root WebApplicationContext가 초기화될 때 AppConfig.class의 컴포넌트 스캔이 실행된다. (ContextLoaderListener의 contextInitialized 메소드가 호출될 때)
        • DispatcherServlet의 WebApplicationContext가 초기화될 때 DispatcherConfig.class의 컴포넌트 스캔이 실행된다. (DispatcherServlet이 초기화될 때)

참고 자료

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Configuration.html https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/config/annotation/EnableWebMvc.html https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/ComponentScan.html https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/config/annotation/EnableWebMvc.html https://stackoverflow.com/questions/28293400/spring-root-and-servlet-context-with-java-config https://catsbi.oopy.io/c760561d-50fd-4874-aa01-17feb45fe980 https://ittrue.tistory.com/229#google_vignette https://steady-coding.tistory.com/594 https://castleone.tistory.com/2 https://mangkyu.tistory.com/75 https://mangkyu.tistory.com/176 https://galid1.tistory.com/532