@Service
public class PetService {
// 서비스 로직
}
//1.@Autowired 방식
@Controller
public class PetController {
@Autowired
private PetService petService;
public void handleRequest() {
petService.doSomething();
}
}
//2. 생성자 주입 방식
@Controller
@RequiredArgsConstructor
public class PetController {
private final PetService petService;
}
위 코드는 스프링부트에서 DI 패턴을 적용한 예시 코드이다. 이 코드를 보면 DI 를 "등록" 하는것에 대해서는 이해할 수 있다. 하지만 DI 를 등록한 이 PetController 를 사용할때 PetService 를 주입해줘야 하는데, 이것은 어디서 주입할까?

스프링에서는 이것을 스프링이 대신 주입을 해준다. 개발자가 애플리케이션 코드로 직접 제어하는 것이 아니라 프레임워크가 객체의 생성과 관리에 대한 책임진다고 해서 제어의 역전이라고 한다. 이렇게 IoC를 활용하면 객체 간의 결합도를 낮추고 코드의 유연성과 확장성을 높이는 설계를 할 수 있다.
예시를 한번 살펴보자.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Service
public class MyService { // MyService 를 @Service 를 통해 Bean으로 등록
}
이 코드에서는 @Service 어노테이션을 사용하여 MyService를 빈으로 등록하였다. 이 등록된 Bean은 스프링 컨테이너에 의해 관리된다.
@Controller
public class MyController {
private final MyService myService;
@Autowired // 생성자 주입을 통해 MyService Bean을 주입받음
public MyController(MyService myService) {
this.myService = myService;
}
public void doSomething() {
myService.performAction(); // 주입받은 MyService Bean을 사용
}
}
MyController에서는 MyService를 필요로한다. 생성자 주입 방식을 통해 MyService 빈을 주입 받는다. Spring이 MyController 객체를 생성할 때, MyService 에서 등록한 Bean을 자동으로 주입한다.
애플리케이션을 개발하다보면 수많은 클래스가 존재하고 의존성을 필요로 할 것이다. 그렇다면 스프링에서는 어떻게 빈의 초기화 순서를 관리할까? 바로 다음과 같은 방식으로 이루어진다.
// @Component, @Service 등을 찾아서
@Service
public class OrderService { ... }
// Bean Definition Registry에 등록
Registry: {
"orderService" → BeanDefinition,
"paymentService" → BeanDefinition,
...
}
그래프 분석:
PaymentGateway (의존성 없음)
↑
PaymentService (PaymentGateway 필요)
↑
OrderService (PaymentService 필요)
생성 순서 결정:
[PaymentGateway, PaymentService, OrderService]
// 1. PaymentGateway 생성
PaymentGateway gateway = new PaymentGateway();
// 2. PaymentService 생성 + 의존성 주입
PaymentService service = new PaymentService(gateway); // gateway 주입!
// 3. OrderService 생성 + 의존성 주입
OrderService orderService = new OrderService(service); // service 주입!
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
// 여기서 에러 발생! 💥
}
}
***************************
APPLICATION FAILED TO START
***************************
Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| serviceA defined in file [ServiceA.class]
↑ ↓
| serviceB defined in file [ServiceB.class]
└─────┘
UnsatisfiedDependencyException를 발생시켜 애플리케이션을 중단한다. (런타임 시에 발견)@Component
public class B {
public B() {
System.out.println("B initialized");
}
}
@Component
public class A {
private final B b;
@Autowired
public A(B b) {
this.b = b;
System.out.println("A initialized");
}
}
//B 클래스는 A 클래스의 생성자에 의해 주입되므로,
//Spring은 먼저 B 클래스를 초기화한 후, A 클래스를 초기화합니다.
//B initialized
//A initialized
DI(Dependency Injection)와 IoC(Inversion of Control)는 현대 소프트웨어 개발에서 핵심적인 설계 원칙으로, 두 개념은 서로 밀접하게 연결되어 있다.
IoC는 객체의 제어 흐름을 애플리케이션 코드에서 프레임워크나 외부 시스템으로 넘기는 것을 의미한다. 이는 객체가 스스로 의존성을 관리하지 않고, 필요한 의존성을 외부에서 제공받도록 함으로써, 객체 간의 결합도를 낮추고 코드의 유연성과 확장성을 높인다.
DI는 IoC를 실현하는 방법 중 하나로, 객체가 필요한 의존성을 외부에서 주입받는 방식이다. DI를 통해 객체는 자신의 의존성을 직접 생성하지 않고, 외부에서 주입받아 사용하게 된다. 이로 인해 코드의 재사용성과 유지보수성이 크게 향상되며, 테스트 환경에서도 객체를 쉽게 대체하거나 Mocking할 수 있어 테스트 용이성도 증가한다.
결론적으로, IoC는 객체의 제어 권한을 외부로 넘기는 원칙이며, DI는 이를 구현하는 구체적인 방법이다. DI를 통해 IoC가 실현되며, 이 두 개념은 함께 사용될 때 객체지향 설계의 유연성을 극대화할 수 있다. 이를 통해 복잡한 애플리케이션에서도 일관성 있고 유지보수 가능한 구조를 설계할 수 있게 된다.