Inversion of Control: 제어의 역전
기존에는 런타임에 대한 객체 간 관계를 개발자가 결정했다.
즉, 제어권이 개발자에게 있었는데 이러한 제어권을 프레임워크에 위임하고 객체 간 연결과계를 런타임에 결정하는 것이다.
그렇게 되면, 객체 간의 관계가 느슨하게 연결되면서 확장에 유리해진다. 그리고 이를 구현하는 방법 중 하나가 DI(Dependency Injection) 의존성 주입이다.
IoC를 구현하는 방법은 DL과 DI가 있다.
@Controller
public class HelloController {
@Autowired
private HelloService helloSerivce;
public HelloController() {}
}
이처럼 필드에 @Autowired
를 써서 객체를 주입하는 방법이다.
@Controller
public class HelloController {
private HelloService helloService;
public HelloController() {}
@Autowired
public void setHelloService(HelloService helloService) {
this.helloService = helloService;
}
}
@Controller
public class HelloController {
private HelloService helloService;
@Autowired
public HelloController(HelloService helloService) {
this.helloService = helloService;
}
}
위 코드의 생성자에 선언되어있는 @Autowired
어노테이션은 생성자가 한 개만 있을 경우 생략 가능하다.
3가지 모두 사용해보면 필드 주입이 코드도 가장 짧고 쉽게 따라할 수 있을 것 같다.
그럼 왜 생성자 주입을 지향할까?
필드 주입의 단점
필드 주입으로 객체를 주입하면 외부에서 수정이 불가능 하다는 점이 있다. 외부에서 수정이 불가능 하다는 것은 수정할 객체 외 다른 외부 객체도 수정해야 한다. 즉, 객체간 결합도가 높아지면서 유지보수에 불리해 진다.
또한, 필드 주입은 DI를 지워하는 프레임워크에서만 사용할 수 있다는 제한이 있다.
수정자 주입의 단점
수정자(메서드) 주입의 경우 public 접근 제한자로 구현된다. 그래서 관계를 주입 받는 객체의 변경 가능성 열어두고 있다. 그래서 수정자 주입은 객체가 변경될 필요성이 있을 때만 사용해야 한다.
하지만, 변경 가능성을 열어두기 때문에 임의로 객체를 변경할 수 있게 되며 이로 인한 에러가 발생할 수 있다.
객체의 불변성 확보
객체 생성 시 1회만 호출되는 것이 보장되는 특징이 있다. 그래서 주입받은 객체가 불변 객체여야 되거나, 반드시 해당 객체의 주입이 필요한 경우 사용한다.
테스트 코드 작성의 편리함
테스트 코드는 Spring과 같은 DI를 지원하는 프레임워크에서만 돌아간다는 보장이 없다. 즉, 테스트 코드를 작성하는 환경에서 필드 주입을 사용하지 못하면 테스트 코드를 작성하는 것에 어려움이 있을 것이다.
필드 주입으로 작성 시 @Autowired로 설정된 필드가 null이 되어 NullPointerException이 발생하게 된다.
순환 참조에 의한 순환 호출 방지
순환 참조란, A객체가 B를 참조하는데 B 역시 A를 참조하는 무한 루프와 같은 현상을 말한다. 이러한 상황이 필드, 수정자 주입에서 발생할 수 있다. 그 코드를 간단하게 구현해 보자면,
@Service
public class A{
@Autowired
private B b;
public void saySomething(){
b.saySomething();
}
}
@Service
public class B{
@Autowired
private A a;
public void saySomething(){
a.saySomething();
}
}
이 코드는 한 객체의 saySomething()를 호출하면 서로 객체를 호출하면서 결국 StackOverflowError가 발생한다. 해당 에러에서 알 수 있듯, 런타임 과정에서 발생하는 에러이기 때문에 서비스 중 에러가 발생 할 수 있다는 큰 리스크가 있다.
그럼, 생성자 주입으로 코드를 작성해 보자.
@Service
public class A{
private B b;
@Autowired
public A(B b) {
this.b = b;
}
public void saySomething(){
b.saySomething();
}
}
@Service
public class B{
@Autowired
private A a;
@Autowired
public B(A a) {
this.a = a;
}
public void saySomething(){
a.saySomething();
}
}
생성자 주입으로 순환 참조를 일으킬 때 가장 큰 장점은 컴파일 에러가 발생한다는 점이다. 즉, 개발 단계에서 해당 로직의 문제를 발견하고 해결할 수 있기에 코드의 안정성이 높아진다.
예전에 필드 주입을 남발했던 과거의 모습을 반성하게 되었다..