Spring을 처음 접하면 @Component, @Autowired 같은 어노테이션이 낯설게 느껴진다.
이 글에서는 Spring의 심장이라고 할 수 있는 두 가지 개념, IoC(제어의 역전) 와 DI(의존성 주입) 를 코드와 함께 차근차근 살펴본다.
new 키워드의 함정다음 코드를 보자.
public class OrderService {
private EmailSender emailSender;
public OrderService() {
this.emailSender = new EmailSender(); // 문제의 코드
}
public void placeOrder(Order order) {
emailSender.send("주문이 완료되었습니다.");
}
}
얼핏 보면 문제없어 보인다. 하지만 이 코드에는 세 가지 문제가 숨어 있다.
테스트 코드를 작성할 때, EmailSender는 실제로 이메일을 발송하는 클래스다.
테스트할 때마다 이메일이 날아가면 곤란하다.
그래서 가짜 구현체(FakeEmailSender)를 만들었다고 해도, 현재 구조에서는 끼워 넣을 방법이 없다.
public class FakeEmailSender extends EmailSender {
@Override
public void send(String message) {
System.out.println("(가짜) 이메일 전송: " + message);
}
}
OrderService 생성자 안에 new EmailSender()가 고정되어 있기 때문에,
FakeEmailSender를 사용하려면 소스 코드를 직접 수정해야 한다.
OrderService의 역할은 주문 처리다.
그런데 지금은 EmailSender 객체를 직접 생성하는 책임까지 지고 있다.
OrderService가 변경 없이 할 수 없는 것들:
EmailSender → SmsSender로 교체FakeEmailSender로 교체LoggingEmailSender로 교체교체할 때마다 OrderService 코드를 열어야 한다.
이것이 결합도가 높다는 의미다.
실제 서비스 규모에서는 이런 상황이 온다.
OrderService orderService = new OrderService(
new EmailSender(
new SmtpConnection(
new NetworkConfig(...)
)
)
);
클래스가 수십, 수백 개로 늘어나면 개발자가 이걸 직접 관리하는 건 사실상 불가능하다.
누락이 생기고, 실수가 생기고, 유지보수가 지옥이 된다.
문제의 핵심은 OrderService가 EmailSender를 직접 만든다는 것이다.
해결책은 간단하다. 직접 만들지 말고, 외부에서 받자.
// Before: 내가 직접 만든다
public OrderService() {
this.emailSender = new EmailSender();
}
// After: 외부에서 받는다
public OrderService(EmailSender emailSender) {
this.emailSender = emailSender;
}
이렇게 하면:
// 실제 서비스
OrderService service = new OrderService(new EmailSender());
// 테스트
OrderService service = new OrderService(new FakeEmailSender()); // 코드 수정 없이!
OrderService는 무엇이 들어오는지 알 필요가 없다.
EmailSender처럼 동작하는 무언가가 오면 그냥 쓸 뿐이다.
이것이 DI(Dependency Injection), 의존성 주입이다.
객체가 필요로 하는 의존성을 직접 생성하지 않고, 외부에서 주입받는 것
DI를 적용했지만 새로운 문제가 생겼다.
클래스가 수백 개면 누가 객체를 만들어서 주입해줄 것인가?
개발자가 직접 한다면:
public class AppConfig {
public OrderService orderService() {
return new OrderService(emailSender());
}
public EmailSender emailSender() {
return new EmailSender(smtpConnection());
}
// ... 500개 계속
}
이것도 결국 사람이 관리하는 한계가 있다.
싱글톤 보장, 생명주기 관리, 순환 의존성 처리... 고려할 것이 너무 많다.
그래서 등장한 것이 IoC Container다.
IoC = Inversion of Control = 제어의 역전
| 기존 방식 | IoC | |
|---|---|---|
| 객체를 누가 만드나 | 개발자가 직접 new | Container가 대신 |
| 의존성을 누가 연결하나 | 개발자가 직접 주입 | Container가 자동으로 |
| 제어권이 어디에 | 개발자 | Framework |
객체 생성과 관리의 제어권이 개발자에서 Framework(Spring)로 넘어간 것,
이것이 "제어의 역전"이라는 이름의 유래다.
@Component
public class EmailSender {
public void send(String message) {
// 이메일 발송 로직
}
}
@Component를 붙이면 Spring이 앱 시작 시 이 클래스를 new해서 보관한다.
이렇게 Spring Container에 보관된 객체를 Bean이라고 부른다.
@Component
public class OrderService {
private final EmailSender emailSender;
@Autowired
public OrderService(EmailSender emailSender) {
this.emailSender = emailSender;
}
public void placeOrder(Order order) {
emailSender.send("주문이 완료되었습니다.");
}
}
@Autowired를 붙이면 Spring이 Container에 보관중인 EmailSender Bean을 꺼내서 생성자에 주입해준다.
앱 시작
↓
@Component 붙은 클래스 스캔
↓
EmailSender 발견 → new EmailSender() 후 보관 (Bean 등록)
↓
OrderService 발견 → 생성자에 EmailSender 필요 확인
↓
보관중인 EmailSender를 꺼내서 new OrderService(emailSender) 완료
↓
개발자는 그냥 OrderService 가져다 씀
개발자가 new를 한 줄도 쓰지 않았다.
실제 웹 요청 처리 흐름을 보면:
@RestController
public class OrderController {
private final OrderService orderService;
@Autowired
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
public void order() {
// OrderController는 EmailSender를 전혀 몰라도 된다
orderService.placeOrder(new Order());
}
}
Spring Container
├── EmailSender Bean 보관
├── OrderService Bean 보관 (EmailSender 주입됨)
└── OrderController Bean 보관 (OrderService 주입됨)
HTTP 요청 들어오면
└── OrderController.order() 호출
└── orderService.placeOrder() 호출
└── emailSender.send() 호출
각 클래스는 바로 아래 계층만 알면 된다.
OrderController는 OrderService만, OrderService는 EmailSender만.
| 개념 | 한 줄 정의 |
|---|---|
| DI (Dependency Injection) | 객체가 필요한 의존성을 외부에서 주입받는 것 |
| IoC (Inversion of Control) | 객체 생성/관리 제어권이 개발자에서 Framework로 넘어가는 것 |
| Bean | Spring Container가 생성하고 관리하는 객체 |
| @Component | "이 클래스를 Bean으로 등록해줘"라고 Spring에게 알리는 어노테이션 |
| @Autowired | "Container에서 해당 Bean을 꺼내 여기에 주입해줘"라는 어노테이션 |
DI는 방법이고, IoC는 목표다.
DI라는 방법으로 IoC라는 목표를 달성한다.
처음 Spring을 접하면 @Component와 @Autowired가 마법처럼 느껴질 수 있다.
하지만 그 배경에는 명확한 문제의식이 있다.
Spring은 이 두 가지를 프레임워크 레벨에서 자동화해준다.
개발자는 비즈니스 로직에만 집중할 수 있게 된다.