🍃Spring

[Spring] IoC와 DI (2/2)

waveofmymind 2023. 5. 29. 11:54

DI 구현 방법

필드 주입

@Service
public class BurgerService {

	@Autowired
    private BurgerRecipe burgerRecipe;
}

변수 선언부에 @Autowired 어노테이션을 붙인다.

  • 장점
    • 사용하기 편하다
  • 단점
    • 단일 책임 원칙 위반의 가능성이 있다.
      • @Autowired 선언만 하면 되므로 의존성을 주입하기 쉽다.
      • 따라서, 하나의 클래스가 많은 책임을 갖게 될 가능성이 높다.
    • 의존성이 숨는다.
      • 생성자 주입에 비해 의존 관계를 한 눈에 파악하기 어렵다.
    • DI 컨테이너와의 결합도가 커지고, 테스트하기 어렵다.
    • 불변성을 보장할 수 없다.
    • 순환 참조가 발생할 수 있다.

수정자 주입

@Service
public class BurgerService {

    private BurgerRecipe burgerRecipe;

	@Autowired
    public void setBurgerRecipe(BurgerRecipe burgerRecipe) {
        this.burgerRecipe = burgerRecipe;
    }
}

setter를 사용한 주입 방법

  • 장점
    • 선택적인 의존성을 사용할 수 있다.
  • 단점
    • 선택적인 의존성을 사용할 수 있다는 것은 BurgerService에 모든 구현체를 주입하지 않아도 burgerRecipe 객체를 생성할 수 있고, 객체의 메소드를 호출할 수 있다. 즉, 주입받지 않은 구현체를 사용하는 메소드에서 NPE가 발생한다.
    • 순환 참조 문제가 발생할 수 있다.

생성자 주입

@Service
public class BurgerService {

    private BurgerRecipe burgerRecipe;

		@Autowired
    public BurgerRecipe(BurgerRecipe burgerRecipe) {
        this.burgerRecipe = burgerRecipe;
    }
}

생성자에 @Autowired 어노테이션을 붙여 의존성을 주입받을 수 있으며, 가장 권장되는 주입 방식이다.

  • 장점
    • 의존 관계를 모두 주입 해야만 객체 생성이 가능하므로 NPE를 방지할 수 있다.
    • 불변성을 보장할 수 있다.
    • 순환 참조를 컴파일 단계에서 찾아낼 수 있다.
    • 의존성을 주입하기 번거롭고, 생성자 인자가 많아지면 코드가 길어져 위기감을 느낄 수 있다.
      • 이를 바탕으로 SRP 원칙을 생각하게 되고, 리팩터링을 수행하게 된다.

순환 참조

순환 참조란 서로 다른 여러 빈들이 서로를 참조하고 있음을 의미한다.

CourseService에서 StudentService에 의존하고, StudentService가 CourseService에 의존하면 순환 참조라고 볼 수 있다.

필드 주입인 경우

@Service
public class CourseServiceImpl implements CourseService {

    @Autowired
    private StudentService studentService;

    @Override
    public void courseMethod() {
        studentService.studentMethod();
    }
}

@Service
public class StudentServiceImpl implements StudentService {

    @Autowired
    private CourseService courseService;

    @Override
    public void studentMethod() {
        courseService.courseMethod();
    }
}

StudentServiceImpl의 studentMethod()는 CourseServiceImpl의 courseMethod() 를 호출하고, CourseServiceImpl의 courseMethod() 는 StudentServiceImpl의 studentMethod() 를 호출하고 있는 상황이다.

서로 주거니 받거니 호출을 반복하면서 끊임없이 호출하다가 StackOverFlow를 발생시키고 죽는다.

 

이처럼 필드 주입이나, 수정자 주입은 객체 생성 후 비즈니스 로직 상에서 순환 참조가 일어나기 때문에, 컴파일 단계에서 순환 참조를 잡아낼 수 없다.

생성자 주입인 경우

@Service
public class CourseServiceImpl implements CourseService {

		private final StudentService studentService;

    @Autowired
    public CourseServiceImpl(StudentService studentService) {
        this.studentService = studentService;
    }

    @Override
    public void courseMethod() {
        studentService.studentMethod();
    }
}

@Service
public class StudentServiceImpl implements StudentService {

    private final CourseService courseService;

    @Autowired
    public StudentServiceImpl(CourseService courseService) {
        this.courseService = courseService;
    }

    @Override
    public void studentMethod() {
        courseService.courseMethod();
    }
}

생성자 주입일 때 애플리케이션을 실행하면 아래 로그가 찍히면서 실행이 실패한다.

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  courseServiceImpl defined in file [/.../CourseServiceImpl.class]
↑     ↓
|  studentServiceImpl defined in file [/.../StudentServiceImpl.class]

이처럼 생성자 주입은 스프링 컨테이너가 빈을 생성하는 시점에 순환 참조를 확인하기 때문에, 컴파일 단계에서 순환 참조를 잡아낼 수 있다.

@Autowired

DI를 할 때 사용하는 어노테이션이며, 의존 관계의 타입에 해당하는 빈을 찾아 주입하는 역할을 한다.

쉽게 말하자면 스프링 서버가 올라갈 때, Application Context가 @Bean이나 @Service, @Controller 등 어노테이션을

이용하여 등록한 스프링 빈을 생성하고, @Autowired 어노테이션이 붙은 위치에 의존 관계 주입을 수행하게 된다.

그렇다면, @Autowired 어노테이션이 붙은 위치에 어떻게 의존 관계를 주입하는 걸까? 우선 @Autowired 어노테이션의 코드를 살펴 보자.

/**
* Note that actual injection is performed through a BeanPostProcessor which in turn means
* that you cannot use @Autowired to inject references into BeanPostProcessor or 
* BeanFactoryPostProcessor types. Please consult the javadoc for the 
* AutowiredAnnotationBeanPostProcessor class (which, by default, checks for the presence 
* of this annotation).
* Since:
* 2.5
* See Also:
* AutowiredAnnotationBeanPostProcessor, Qualifier, Value
* Author:
* Juergen Hoeller, Mark Fisher, Sam Brannen
*/
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
	boolean required() default true;

}
  • @Target
    • 생성자와 필드, 메소드에 적용 가능하다.
  • @Retention
    • 컴파일 이후(런타임 시) JVM에 의해 참조가 가능하다. 런타임 시 이 어노테이션의 정보를 리플렉션으로 얻을 수 있다.

위 코드 상단의 주석을 보면, 실제 타깃에 Autowired가 붙은 빈을 주입하는 것은 BeanPostProcessor라는 내용을 찾을 수 있고, 그것의 구현체는 AutowiredAnnotationBeanPostProcessor인 것을 확인할 수 있다.

AutowiredAnnotationBeanPostProcessor 클래스

AutowiredAnnotationBeanPostProcessor 클래스가 실제 타깃에 빈을 주입하는 역할을 한다.

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

해당 클래스에는 processInjection() 메소드가 있는데, @Autowired로 어노테이디드된 필드나 메소드에 대해서 객체를 주입하는 역할을 한다. 이 메소드 안을 보면, InjectionMetadata 클래스의 inject() 메소드가 있다, 이것도 한 번 살펴 보자.

public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
		Collection<InjectedElement> checkedElements = this.checkedElements;
		Collection<InjectedElement> elementsToIterate = (checkedElements != null ? checkedElements : this.injectedElements);
		if (!elementsToIterate.isEmpty()) {
				for (InjectedElement element : elementsToIterate) {
					element.inject(target, beanName, pvs); //아래 inject() 메소드 호출
				}
		}
}

protected void inject(Object target, @Nullable String requestingBeanName, @Nullable 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();
				}
		}
}

해당 메소드는 객체를 주입할 때 ReflectionUtils 클래스를 사용하는 것을 볼 수 있다. 즉, @Autowired는 리플렉션을 통해 수행된다.

리플렉션이란?

리플렉션은 구체적인 클래스 타입을 알지 못해도, 그 클래스의 메서드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API

 

출처