Spring DI?

Spring DI는 스프링이 내부에 있는 Bean 간의 관계를 관리, 주입할 때 사용하는 기법이다.

 

스프링 컨테이너를 통해 관리되고 있는 POJO들을 비즈니스 코드에서 필요한 시점에 선택적으로 주입받을 수 있으며 이후 관리는 모두 컨테이너가 담당한다. Singleton Scope Bean

 

DI를 통해 객체 외부에서 생성된 객체(Bean)를 Interface를 통하여 넘겨받게 되므로, 객체 간의 결합도를 낮출 수 있고, 런타임 시에 의존관계가 결정되기 때문에 유연한 구조를 가질 수 있다. 이를 통해 진정한 의미의 컴포넌트화를 가능케한다.

 

 

 

DI Annotations

 

 

 

@Autowired (AutowiredAnnotationBeanPostProcessor)

우선 빈의 타입을 기준으로 찾아서 주입하는 방식 (타입 → 이름 → @Qualifier)

 

 

사용할 수 있는 위치

  • 생성자
  • 세터
  • 필드

 

DI시 예외가 발생할 수 있는 경우

  • (컨테이너 안에) 해당 타입의 빈이 없는 경우 (NoSuchBean)
  • (컨테이너 안에) 해당 타입의 빈이 한 개인 경우 (정상 작동)
  • (컨테이너 안에) 해당 타입의 빈이 여러 개인 경우

 

해당 타입의 빈이 여러 개인 경우 (모든 경우)

 

1. @Primary의 유무를 확인한 후 주입한다. (우선순위 부여)

@Service 
@Primary 
public class RealService implements XxxService {

 

2. @Order 기준으로 확인한 후 주입한다. (빈들의 우선순위 설정)

@Service 
@Order(1) 
public class RealService implements XxxService {

 

3. Bean의 이름(빈의 ID = Class 명의 Small case)을 기준으로 주입한다.

 

4. @Qualifier 가 설정되어 있는지를 기준으로 확인한 후 주입한다.

@Qualifier("realService")


public RealController (@Qualifier("realService") RealService realService) { 
	this.realService = realService; 
}

 

 

5. 혹은 모든 빈을 주입받기. (Collection 사용)

@Autowired 
private List<RealService> realService;

 

6. 필드명을 작성해서 넣기

@Service
public class RealService implements XxxService{

-------------------------------------------

    @Autowired
    RealService realService;

 

 

 

 

@Resource (CommonAnnotationBeanPostProcessor)

우선 빈의 ID (이름) 값을 기준으로 주입하는 방식 @Resource(name="beanName")

 

 

사용할 수 있는 위치

  • 필드
  • 세터

 

해당 타입의 빈이 여러 개인 경우

  1. @Primary
  2. @Order
  3. Bean Type
  4. @Qualifier

 

 

 

@Inject (JSR-330's : AutowiredAnnotationBeanPostProcessor)

Autowired처럼 빈 타입을 기준으로 찾아서 주입하는 방식 (타입 → @Qualifier → 이름)

 

 

사용할 수 있는 위치

  • 필드
  • 세터
  • 생성자
  • 메서

 

해당 타입의 빈이 여러 개인 경우

  1. @Primary
  2. @Order
  3. @Qualifier
  4. Bean 이름

 

 

Spring DI 방식

 

Constructor Injection

Bean을 등록하기 위한 Component Scan 중에 해당 Class 정보를 확인하게 된다.

 

Spring 4.2까지는 Autowired를 붙여야 한다.

 

 

해당 방식의 특징

  • 객체 생성 시 호출되는 생성자를 통해 1번만 주입되는 것을 보장한다.
  • 불변적이며, 우선적으로 주입된다.
  • Spring에서는 필수적인 의존성 주입에 대하여서 생성자 주입을 권장한다.
@Service 
public class UserService { 

    private final UserRepository userRepository; 
    
    // 해당 클래스가 다른 생성자를 가지지 않는다면 생략할 수 있다. Spring 4.3+ 
    // 그리고 추가적으로 Configuration 클래스에도 생성자 주입을 할 수 있게 되었다. 
    @Autowired 
    public UserAccountService(UserRepository userRepository) { 
    	this.userRepository = userRepository; 
    } 
    
    // Logic... 
    
}

 

해당 방식의 장점

  • 단위 테스트가 용이하다.
  • Bean을 생성하는 동안 다른 Bean을 초기화할 수 있다. 별도의 값을 저장한다던지..
  • 클래스 레벨에서 불변한 객체 주입이 가능하다.만약에 주입 대상이 되는 Bean들이 없거나, 오류로 인해 주입되지 않을 경우 Checked Exception을 통하여 알아차릴 수 있다. = NPE를 방지한다.
  • final Keyword와 생성 시점의 주입을 통해 갑작스러운 변경에 의한 문제를 해결하게 된다.
  • 쉽게 불변 객체를 만들어낼 수 있다.
  • 안정적인 상태를 유지할 수 있다. → 모든 의존성이 주입되거나 인스턴스화를 실패한다.
  • Reflection API를 사용하지 않는다. 주입 시에 Spring 상에서 최소의 자원을 사용한다.
  • 생성자를 사용하여 인스턴스를 생성하는 것이 OOP 관점에서 자연스럽다.

 

 

해당 방식의 단점

  • 해당 Bean이 선택적인 종속성을 가져야 하는 경우 가변적인 상태
  • Setter Injection을 사용하자. 생성자 주입과 설정자 주입은 결합이 가능하다.

 

 

이것들은 단점일까?

  • 해당 Bean이 적은 가짓수의 의존성을 가질 경우
  • 해당 Bean의 필수 의존성이 적을 경우 생성자 주입 코드는 "장황해 보일 수도" 있다.
  • 해당 Bean이 아주 많은 가짓수의 의존성을 가질 경우 Ugly Constructor
  • 종속성이 추가될 때마다 생성자의 매개 변수에 추가해야 하므로 생성자를 통해 과도한 결합을 노출하게 된다.

 

추악한 생성자? ( Ugly Constructor? )

클래스가 수많은 필드(의존성)를 가지고 작성되어 있다면, 생성자도 복잡해질 수밖에 없다.

// 점층적 생성자 패턴
public class Employee {

        // 필수적인 속성, 의존성
        private final String firstname;
        private final String lastname;

        // 선택적인 속성, 의존성
        private String email;
        private String address;
        private String phone;

        // Default Constructor 는 필수적인 의존성(final) 을 포함하여야한다. 
        public Employee(String firstname, String lastname) {
            this.firstname = firstname;
            this.lastname = lastname;
        }

        public Employee(String email, String address, String phone) {
            this.email = email;
            this.address = address;
            this.phone = phone;
        }

        public Employee(String firstname, String lastname, String email, String address, String phone) {
            this.firstname = firstname;
            this.lastname = lastname;
            this.email = email;
            this.address = address;
            this.phone = phone;
        }
}

사실 가정하는 상황에서는 이것보다 더 많은 인자를 가지고 있을 것이다. 그럴 경우에는 생성자가 더욱 복잡해질 것이고, 객체의 코드를 작성하거나 파악하는 것에 어려움을 겪을 것이다.

 

Spring의 경우에는 수많은 Service 혹은 Repository의 생성자 주입을 말할 수 있다.

 

사실 이러한 생성자가 만들어진다는 것은 그 객체의 책임이 혼재되어 있거나 과중한 책임을 지니고 있음을 의미하는 것이다.

 

그렇기에 이것은 생성자 주입의 문제로만 치부하기보다는 설계의 문제임을 생각하는 것이 좋다고 생각한다.

 

 

Setter Injection

생성된 Bean 정보들 중에서 @Autowired가 붙은 Setter를 찾아 호출하며 Bean을 주입한다.

 

Method Injection도 비슷하게 동작한다.

 

 

해당 방식의 특징

  1. 자바 빈 규약(setter)을 이용한 의존성 주입 방식이다.
  2. Spring에서는 변경 가능성 있는, 선택적인 종속성에 대해서 설정자 주입을 권장한다.
@Service
public class UserService {

        private final UserRepository userRepository;

        // 특별한 요청에 경우 필요하다고 가정. 
        private GuestRepository guestRepository;

    public UserAccountService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

        @Autowired
        public void setGuestRepository(GuestRepository guestRepository) {
                this.guestRepository = guestRepository;
        }

        // Logic...

}

 

해당 방식의 장점

  • 선택적인 종속성에 대하여서 별도의 설정자를 만들고 유연한 제공이 가능하다.

 

해당 방식의 단점

  • 의존성이 주입되는 시점이 많아질 수 있기에 가독성이 떨어지게 되고, 상대적으로 유지보수 능력을 떨어트린다.
  • 설정자가 호출될 때마다 객체가 새로 주입됨으로 참조에 대한 불변을 유지할 수 없다.
  • Null이 주입될 수 있고, 새로운 객체의 반환으로 기존 작업 값을 잃을 수 있다.
  • 설정자를 통해 클래스 내부의 의존성 부분을 노출함으로써 캡슐화를 위반하게 된다.
  • 객체 생성 시점에는 순환 종속성이 발견되지 않으나, 비즈니스 로직이 진행되는 도중에 해당 문제가 발생할 수 있다.

 

 

Field Injection

필드 주입을 위해 Reflection API를 이용하여 정보를 찾고 접근 제어자 등을 조작한다.

@Service
public class UserService {

        // 간단한 주입방식!
        @Autowired private UserRepository userRepository;


        // Logic...

}

 

해당 방식의 장점

  • 모든 주입 방식 중 가독성 측면에서 가장 간결하게 사용할 수 있다.생성자와 수정자 코드 등
  • 필드 주입을 선택하는 이유 중 하나는 보일러 플레이트 코드를 작성하지 않기 위함이다.
  • 많은 필드를 갖는 것이 불가피하고 추악한 생성자가 문제가 되는 상황에서는 필드 주입을 사용하는 것이 "나을 수도" 있다. 별도의 Null 체크가 필요하고.. 이건 설계 문제다!
  • 그런 상황에서 필드 주입을 선택하는 것이 최선인지는 의문이 든다.

사용할 것이라면..

  • Application 코드와 관계없는 테스트 코드에 사용 (Mock 객체 주입 등)
  • 스프링 설정을 목적으로 하는 @Configuration 클래스에서만 사용

 

해당 방식의 단점

  • Private 한 필드에 의존성을 주입할 수 있는 유일한 방법은 스프링 컨테이너가 클래스를 인스턴스 화하고 Reflection API를 사용하여 주입하는 것이다.
  • 이러한 주입 방식은 생성자, 설정자 주입 방식보다 많은 자원을 소모한다.
  • Spring의 DI에 의존적인 코드를 작성하게 된다. 순수한 자바 코드가 아니다.
  • 의존성 주입을 실패하게 된 경우 Exception이 터지지 않고 참조가 연결되지 않기에 비즈니스 로직 시에 문제가 발견될 수 있다
  • Bean을 찾지 못하였거나, 여러 Bean이 존재하고, Primary, Qualifier, Order 등이 설정되지 않은 경우

 

 

 

생성자 주입은 필드, 메서드 방식보다 적게 Reflection을 사용한다.

bean을 생성할 때, 다른 bean 정보를 가져오는 Dependency Lookup까지만 사용한다.

 

생성자 주입이 Reflection을 사용하지 않는다는 것은 Bean 생성 시에 주입하기에 다른 방식과 달리 추가적인 비용이 없음을 의미한다.

 

생성자 주입의 실행 시점 Stack trace

AbstractApplicationContext.finishBeanFactoryInitialization() 
→ defaultListableBeanFactory.preInstantiateSingletons() →

AbstractBeanFactory.getBean() → AbstractBeanFactory.doGetBean() →

defaultSingletonBeanRegistry.getSingleton() →

AbstractBeanFactory.getObject() → this.createBean(postController, bean definition) →

AbstractAutowireCapableBeanFactory.createBean() → this.doCreateBean() → 
this.createBeanInstance() → this.autowireConstructor() →

ConstructorResolver.autowireConstructor() →
SimpleInstantiationStrategy.instantiate() →
BeanUtils.instantiateClass() →
Constructor.newInstance() →
DelegatingConstructorAccessorImpl.newInstance() →

NativeConstructorAccessorImpl.newInstance() → (native) this.newInstance0() → 
Bytecode Generation

Constructor와 관련된 Reflection을 제외하고는 다른 Reflection이 보이지 않는다.

 

Field, Method Injection은 Bean이 생성된 이후 AutowiredAnnotationBeanPostProcessor의 processInjection() 메서드를 통해서 주입하게 된다.

public void processInjection(Object bean) throws BeansException {
    Class<?> clazz = bean.getClass();
    InjectionMetadata metadata = findAutowiringMetadata(clazz);
    try {
        metadata.inject(bean, null, null);
    }
    catch (Throwable ex) {
        throw new BeanCreationException("Injection of autowired dependencies failed for class [" + clazz + "]", ex);
    }
}

protected void inject(Object target, String requestingBeanName, PropertyValues pvs) throws Throwable {
    if (this.isField) {
        Field field = (Field) this.member;
        ReflectionUtils.makeAccessible(field);
        field.set(target, getResourceToInject(target, requestingBeanName));
    }
    else {
        if (checkPropertySkipping(pvs)) {
            return;
        }
        try {
            Method method = (Method) this.member;
            ReflectionUtils.makeAccessible(method);
            method.invoke(target, getResourceToInject(target, requestingBeanName));
        }
        catch (InvocationTargetException ex) {
            throw ex.getTargetException();
        }
    }
}

 

생성자 주입이 순환 참조를 찾을 수 있는 이유?

생성자 주입은 Field, Method 방식과 동작하는 시점이 다르다.

 

이는 Bean이 Container에 등록된 시점에서 다른 Bean을 생성하고 주입하기 때문이다.

 

생성자 주입은 자바 코드 그대로 해당 객체를 생성하는데 필수적인 요소를 지정하는 것이다. 그렇기에 A가 필수적인 인자로 B를 가지고 동일하게 B가 A를 가진다면 당연히 Bean 생성 그 자체가 되지 않는 것이다.

 

스프링은 이를 확인하고 순환 참조임을 알려준다.

 

생성자 이외의 유형의 주입을 사용하는 경우 종속성이 필요할 때 주입되기에. 생성자 주입이 진행되는 컨텍스트로드 시점이 아니므로 순환 참조가 발견되지 않는다. 결국 Autowired는 우선 Dependency LookUp을 application context에 존재하는 빈 정보를 가져오고 Injection 하는 것이기에 선택적인 인자 주입이 가능하다.

 

 

잘못된 내용은 댓글 부탁드립니다.

+ Recent posts