IoC( Inversion of Control )는 제어의 역전 이라는 뜻으로 메소드나 객체의 호출작업을 개발자가 결정하는 것이 아니라 외부에서 결정되는 것을 의미합니다.
즉, 객체나 프로그램의 일부에 대한 제어를 컨테이너나 프레임워크로 이전하는 소프트웨어 엔지니어링의 원칙입니다. 사용자 정의 코드가 라이브러리를 호출하는 기존 프로그래밍과 달리 IoC에서는 프레임워크가 프로그램 흐름을 제어하고 사용자 정의 코드를 호출할 수 있습니다.
간단한 예시를 통해 설명하면
기존의 제어의 흐름이 A -> B 이고
이때, B(interface)를 구현한 C 객체를 A에 주입 시킵니다.
그렇게 된다면 제어의 B -> A 로 제어의 흐름이 역전되는 것입니다.
정리해보면, "DI 패턴을 사용하여 IoC 설계 원칙을 구현하고 있다" 이를 통해 객체간의 결합을 약하게 만들고 유연하면서 확장성까지 뛰어난 코드를 작성할 수 있게 Spring 에서 IoC, DI 라는 핵심 기능을 통해 도와주는 것입니다.
// 주문 클래스 (Order) - 의존성을 주입받음
public class Order {
private Payment payment;
public Order(Payment payment) {
this.payment = payment;
}
public void processOrder() {
// 결제 로직 수행
payment.pay();
System.out.println("주문이 처리되었습니다.");
}
}
// 결제 클래스 (Payment)
public class Payment {
public void pay() {
System.out.println("결제가 완료되었습니다.");
}
}
// 주문을 처리하는 곳
public class Main {
public static void main(String[] args) {
Payment payment = new Payment();
Order order = new Order(payment);
order.processOrder();
}
}
// 새로운 결제 클래스 추가 (신용카드 결제)
public class CreditCardPayment extends Payment {
@Override
public void pay() {
System.out.println("신용카드로 결제가 완료되었습니다.");
}
}
// 주문을 처리하는 곳
public class Main {
public static void main(String[] args) {
// 기존 결제 방식
Payment payment = new Payment();
Order order = new Order(payment);
order.processOrder();
// 새로운 결제 방식
CreditCardPayment creditCardPayment = new CreditCardPayment();
Order orderWithCreditCard = new Order(creditCardPayment);
orderWithCreditCard.processOrder();
}
}
1) 필드 주입
// 의존성을 주입받을 클래스
public class Car {
@Autowired // 필드 주입
private Engine engine;
public void start() {
engine.start();
System.out.println("Car ON");
}
}
// 의존성을 주입할 클래스
public class Engine {
public void start() {
System.out.println("Engine ON");
}
}
// 주입하는 곳
public class Main {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
Car car = context.getBean(Car.class);
car.start();
}
}
2) 생성자 주입
// 의존성을 주입받을 클래스
public class Car {
private Engine engine;
// 생성자 주입
public Car(Engine engine) {
this.engine = engine;
}
public void start() {
engine.start();
System.out.println("Car ON");
}
}
// 의존성을 주입할 클래스
public class Engine {
public void start() {
System.out.println("Engine ON");
}
}
// 주입하는 곳
public class Main {
public static void main(String[] args) {
Engine engine = new Engine();
Car car = new Car(engine);
car.start();
}
}
3) Setter 주입
// 의존성을 주입받을 클래스
public class Car {
private Engine engine;
// 세터 주입
public void setEngine(Engine engine) {
this.engine = engine;
}
public void start() {
engine.start();
System.out.println("Car ON");
}
}
// 의존성을 주입할 클래스
public class Engine {
public void start() {
System.out.println("Engine ON");
}
}
// 주입하는 곳
public class Main {
public static void main(String[] args) {
Engine engine = new Engine();
Car car = new Car();
car.setEngine(engine);
car.start();
}
}
정리해보면,
- Bean 이란 스프링이 관리하는 클래스다.
- @Autowired는 우리가 주입을 하기 위해서 주입해달라고 스프링에게 알려주는 어노테이션이다.
- @Autowired를 통해 주입을 할 수 있는 객체는 꼭 Bean 이어야 한다.
- Bean 으로 등록하고 위해서는 @Controller, @Service와 같은 어노테이션을 클래스 위에 등록해야한다.
( Bean을 등록하는 방법 중 하나는 @Component 어노테이션을 사용하는 것인데, @Controller, @Service, @Repository는 @Component를 포함하고 있기 때문에 가능 )
결론, 필드 주입은 테스트 시 활용하기 좋고 대부분의 경우에는 생성자 주입을 사용하자!
위 예제 코드에서 Car 클래스는 생성자 주입을 사용하여 Engine 클래스를 의존성으로 주입받고 있습니다. 이렇게 하면 필수적으로 필요한 의존성을 생성자 매개변수로 명시하고, 불변성을 보장하며, 순환 의존성을 방지할 수 있습니다. 또한 단위 테스트에서도 쉽게 의존성을 Mock 객체로 대체하여 테스트할 수 있기에 스프링 공식 문서에서도 생성자 주입을 권고하고 있습니다.
[참고]https://beststar-1.tistory.com/33
[참고]https://dev-coco.tistory.com/70
[참고]https://sabarada.tistory.com/67