[Spring] IoC와 DI (2/2)
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
출처
- 토비의 스프링 3.1
- https://tecoble.techcourse.co.kr/post/2021-04-27-dependency-injection/
- https://velog.io/@gillog/Spring-DIDependency-Injection-세-가지-방법
- https://jurogrammer.tistory.com/79
- https://yaboong.github.io/spring/2019/08/29/why-field-injection-is-bad/
- https://keichee.tistory.com/446
- https://kellis.tistory.com/58
- https://beststar-1.tistory.com/40
- https://jwchung.github.io/DI는-IoC를-사용하지-않아도-된다