ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [03] ApplicationContext - @Autowired를 통한 Dependency Injection
    Spring/Spring 핵심 기술 2020. 4. 13. 12:02
    반응형

    생성하고자 하는 bean을 A라 하고 A를 생성하기 위해 필요한 또다른 bean을 B라 할 때 @Autowired는 bean A 생성 시 bean B를 IoC(Inversion of Control) Container로 부터 주입받기 위한 annotation입니다.

     

    반드시 IoC Container에 Bean으로 등록된 객체만이 @Autowired를 통해 다른 Bean들에게 주입될 수 있습니다.

     

    예를 들어 다음과 같이 Bean이 아닌 객체의 class가 있고

    public class PetRepository {
    	public Pet save(Pet pet) { return null;}
    }

     

    Bean을 생성할 때 위의 PetRepository class로 생성한 Bean이 아닌 객체를 주입하려고 시도해보면

    @Service
    public class PetService {
        @Autowired
        public PetRepository petRepository;
        
        public PetService(PetRepository petRepository) {
        	this.petRepository = petRepository;
        }
    }

     

    실행창에 "Parameter 0 of constructor ... that could not be found." 라는 오류 메시지를 확인하실 수 있습니다. 필요로 하는 특정한 type의 Bean을 찾을 수 없었다는 메시지입니다.

     

     

     

    @Autowired는 생성자, setter, field 세군데에서 사용할 수 있습니다.

     

     

    [생성자를 통한 Dependency Injection의 예]

    주입하고자 하는 Bean을 생성자의 매개변수로 받아 동일 타입의 멤버변수에 대입합니다.

    @Service
    public class PetService {
        private PetRepository petRepository;
        
        @Autowired
        public PetService(PetRepository petRepository) {
        	this.petRepository = petRepository;
        }
    }

    생성자를 통한 Dependency Injection 시 @Autowired는 생략 가능합니다. 또한 생성자의 특성상 객체생성 시 반드시 호출되기 때문에 주입 받아야 하는 Bean이 없는 경우 Bean(여기서는 PetService Bean)을 생성할 수 없습니다.

     

     

    [Setter를 통한 Dependency Injection의 예]

    다음과 같이 Setter를 통해서도 Dependency Injection이 가능합니다.

    @Service
    public class PetService {
        private PetRepository petRepository;
        
        @Autowired
        public setPetRepository(PetRepository petRepository) {
        	this.petRepository = petRepository;
        }
    }

     

    생성자 주입과는 달리 주입하고자 하는 PetRepository Bean이 없어도 PetService Bean을 생성할 수 있을것 처럼 보입니다만 @Autowired가 붙은 경우 Spring에서 필요로 하는 Bean을 찾아서 주입하고자 하기 때문에 오류가 발생합니다. 따라서 주입받을 Bean이 없는 경우 이를 무시하고 현재 Bean을 생성하고자 한다면 다음과 같이 @Autowired에 required 옵션을 false로 변경해줍니다.

     

    @Service
    public class PetService {
        private PetRepository petRepository;
        
        @Autowired(required = false)
        public setPetRepository(PetRepository petRepository) {
        	this.petRepository = petRepository;
        }
    }

    이렇게 하면 주입할 Bean이 있는 경우는 주입하고 없으면 무시하고 넘어갑니다. required 옵션은 Field를 통한 Dependency Injection에도 동일하게 사용가능합니다.

     

     

    [Field를 통한 Dependency Injection의 예]

    주입하기 원하는 Bean과 동일한 type의 field에 @Autowired를 붙여 Dependency Injection 할 수 있습니다.

    @Service
    public class PetService {
        
        @Autowired(required = true)
        private PetRepository petRepository;
    }

     

     

    [IoC Container 안에 동일한 Type의 Bean이 여러개인 경우의 Dependency Injection]

    예를 들어 Bean의 기능을 정의한 하나의 interface가 있고 이것을 implementation한 구현체가 두개 이상 있다고 할 때 interface 타입으로 Bean을 주입받고자 하면 어떻게 될까요? ApplicationContext는 여러개의 Bean 중 어떤 Bean을 주입해야할지 선택할 수 없어 오류가 발생합니다.

     

    테스트를 위해 interface를 하나 선언해 보겠습니다.

    public interface PetRepository {
    
    }

     

    위의 interface를 구현한 두 class를 선언합니다.

    @Repository
    public class DavePetRepository implements PetRepository {
    
    }
    @Repository
    public class MyPetRepository implements PetRepository {
    
    }

     

    PetRepository type(상위 type)의 Bean을 주입해봅니다.

    @Service
    public class PetService {
    
        @Autowired(required = true)
        private PetRepository petRepository;
    }

     

    이런경우 petRepository는 DavePetRepository일까요? MyPetRepository일까요? 알 수가 없습니다.

    하나의 Bean만 필요한데 두개가 제공 되었으므로 Spring에게 어떤 Bean을 주입할 것인지 명시적으로 알려 주거나 모든 Bean을 다 주입받도록 처리해야 합니다.

     

    1.@Primary로 Bean의 중입 우선 순위를 지정하여 처리

    동일 타입의 Bean 중 우선적으로 주입할 Bean에 @Primary를 붙여줍니다.

    @Repository
    @Primary
    public class DavePetRepository implements PetRepository {
    	public void printPetRepository() {
        	System.out.println(DavePetRepository.class);
        }
    }

     

    나머지 동일 타입의 Bean들은 그대로 둡니다.

    @Repository
    public class MyPetRepository implements PetRepository {
    	public void printPetRepository() {
        	System.out.println(MyPetRepository.class);
        }
    }

     

    실제로 어떤 Bean이 주입 되었는지 확인하기 위해 Runner class를 만들어 결과를 확인해볼 수 있습니다.

    @Component
    public class MyPetRepository implements ApplicationRunner {
        
        @Autowired
        PetRepository petRepository;
        
        public void run(ApplicationArguments args) throws Exception {
        	petRepository.printPetRepository();
        }
    }

     

     

    2.@Qualifier를 사용하여 처리

    @Qualifier는 특정 Bean Id를 지정하여 해당 Bean을 Injection 하도록 하는 어노테이션입니다. Bean은 IoC Container에 등록될 때 Id가 생성되는데 Id는 Bean의 class명을 small case로 시작하는 것입니다.

     

    아래는 @Qualifier를 사용하여 주입하고자 하는 Bean의 Id를 지정하여 원하는 Bean을 주입받는지 확인해보는 코드입니다.

    @Component
    public class AppRunner implements ApplicationRunner {
    
        @Autowired
        @Qualifier("davePetRepository")
        PetRepository petRepository;
        
        public void run(ApplicationArguments args) throws Exception {
        	petRepository.prinPetRepository();
        }
    }

     

    @Primary가 @Qualifier보다 type safe하므로 Bean의 주입 우선순위를 지정할 때는 되도록 @Primary를 사용하는 것이 좋습니다.

     

     

    3.동일 type의 모든 Bean을 주입받는 방법

    List<BeanType>을 사용하여 여러개의 Bean을 주입받을 수 있도록 처리할 수 있습니다.

     

    우선 테스트 결과를 확인하기 위해 AppRunner class를 다음과 같이 작성합니다.

    @Component
    public class AppRunner implements ApplicationRunner {
    
        @Autowired
        PetService petService;
        
        public void run(ApplicationArguments args) throws Exception {
        	petService.printPetRepository();
        }
    }

     

    List<BeanType>의 list에 동일 type의 Bean들을 주입 후 출력합니다.

    @Service
    public class PetService {
        
        @Autowired
        List<PetRepository> petRepositories;
        
        public void printPetRepository() {
        	this.petRepositoryes.forEach(System.out::println);
        }
    }

     

     

     

    [@Autowired 사용 시 Bean의 이름으로 주입할 Bean을 결정하는 예]

    @Autowired는 type을 확인한 뒤에 Bean의 Id로도 확인하여 주입합니다. 즉, 동일 type의 Bean이 여러개여도 Bean이 저장될 filed변수명을 Bean Id와 일치하도록 지정하면 해당 Bean을 주입받습니다. (추천되는 방법은 아닙니다.) 다만, @Primary나 @Qualifier로 지정한 경우가 암시적으로 Bean Id와 filed 변수명을 일치 시킨 것보다 우선순위가 높은 것에 유의합니다.

     

    주입되는 Bean type의 field 변수명을 Bean Id와 동일하게 지정하고 실행해보면 @Primary나 @Qualifier 없이도 원하는 Bean이 선택되어 주입됨을 확인할 수 있습니다.

    @Service
    public class PetService {
        
        @Autowired
        PetRepository petRepository;
        
        public void printPetRepository() {
        	System.out.println(petRepository.getClass());
        }
    }

     

     

     

    이렇게 편리하게 Dependency Injection을 할 수 있도록 해주는 @Autowired가 동작하는 원리가 무엇일까요?

    @Autowired는 BeanPostProcessor interface의 구현체에 의해 처리됩니다. BeanPostProcessor는 Bean Initializing 전,후로 수행될 작업들에 대해 정의해 놓은 interface입니다.

     

    다음 그림은 Bean life cycle의 일부입니다. life cycle의 진행에 따라 먼저 Bean이 생성되고 Bean Initializing 전 후로 BeanPostProcessor가 실행됨을 확인할 수 있습니다.

     

    Bean Life Cycle

     

     

    만약 Bean 생성 직후에 수행하고자 하는 작업이 있다면 @PostConstruct 어노테이션을 사용하거나 InitializingBean interface를 구현하여 처리할 수 있습니다.

     

    1.@PostConstruct를 사용한 예는 다음과 같습니다.

    @Service
    public class PetService {
        
        @Autowired
        PetRepository myPetRepository;
        
        public void printPetRespository() {
        	System.out.println(myPetRepository.getClass());
        }
        
        @PostConstruct
        public void setUp() {
        	//Bean 생성 후 실행할 작업
        }
    }

     

    2.InitializingBean interface를 구현하는 예는 다음과 같습니다. afterPropertiesSet( ) 메서드를 재정의 하여 처리합니다.

    @Service
    public class PetService implements InitializingBean {
        
        @Autowired
        PetRepository myPetRepository;
        
        public void printPetRepository() {
        	System.out.println(myPetRepository.getClass());
        }
        
        @Override
        public vlid afterPropertiesSet() throws Exception {
        	//Bean 생성 후 수행할 작업
        }
    }

     

     

     

     

    @PostConstruct를 사용하면 위의 Bean 주입 상태를 확인하기 위해 선언한 AppRunner class를 제거하고도 동일한 작업을 할 수 있습니다.

     

    우선 Bean 생성 직후 실행될 메서드에 @PostConstruct를 붙여줍니다.

    @Service
    public class PetService {
        
        @Autowired
        PetRepository petRepository;
        
        public void printPetRepository() {
        	System.out.println(myPetRepository.getClass());
        }
        
        @PostConstruct
        public void setUp() {
            //myPetRepository Bean의 printPetRepository()를 Bean 생성 직후 실행
            myPetRepository.printPetRepository();
        }
    }

    위의 코드로 원하는 결과를 확인할 수 있습니다. 다만 Runner class를 사용했을 때와는 결과가 출력되는 위치가 다른데, 그 이유는 Runner class를 사용했던 경우는 application이 완전히 구동한 뒤 메시지가 출력된 것이고 위위 소스코드에서는 application 구동 중 BeanPostProcessor cycle이 실행되는 시점에 메시지가 출력 되었기 때문입니다.

     

     

     

    @Autowired가 작동하는 것은 BeanPostProcessor interface를 상속 받은 AutowiredAnnotationBeanPostProcessor에서 Bean Initializing life cycle 직전에 @Autowired가 붙어있는 부분을 찾아 필요한 Bean을 주입해주기 때문입니다.

     

    Spring에서 @Autowired를 처리하기 위한 작업 순서를 나열하면 다음과 같습니다.

    1. ApplicationContext(BeanFactory)가 BeanPostProcessor(AutowiredAnnoationBeanPostProcessor) Bean 실행.
    2. BeanPostProcessor에서 등록된 Bean들에 대해 BeanPostProcessor에 정의된 로직들을 적용

    참고로 ApplicationContext와 BeanPostProcessor 모두 Spring Application 실행 시 Bean으로 등록됩니다. 다음과 같이 Runner class를 통해 AutowiredAnnotationBeanPostProcessor가 Bean으로 등록되었는지 확인해볼 수 있습니다.

    @Component
    public class AppRunner implements ApplicationRunner {
        
        @Autowired
        ApplicationContext applicationContext;
        
        public void run(ApplicationArguments args) throws Exception {
        	System.out.println(applicationContext.getBean(AutowiredAnnotationBeanPostProcessor.class));
        }
    }

     

    모든 Bean은 @Autowired로 주입받을 수 있고 심지어 모든 Bean을 관리하고 있는 Bean인 ApplicationContext 조차도 Bean으로 주입받아 확인해볼 수 있습니다.

    댓글

Designed by Tistory.