[Spring] IoC와 DI 이해하기

Raha·2026년 3월 20일

Spring

목록 보기
1/6

IoC와 DI

들어가며

Spring을 처음 접하면 @Component, @Autowired 같은 어노테이션이 낯설게 느껴진다.
이 글에서는 Spring의 심장이라고 할 수 있는 두 가지 개념, IoC(제어의 역전)DI(의존성 주입) 를 코드와 함께 차근차근 살펴본다.


1. 문제의 시작: new 키워드의 함정

다음 코드를 보자.

public class OrderService {

    private EmailSender emailSender;

    public OrderService() {
        this.emailSender = new EmailSender(); // 문제의 코드
    }

    public void placeOrder(Order order) {
        emailSender.send("주문이 완료되었습니다.");
    }
}

얼핏 보면 문제없어 보인다. 하지만 이 코드에는 세 가지 문제가 숨어 있다.

문제 1: 테스트가 어렵다

테스트 코드를 작성할 때, EmailSender는 실제로 이메일을 발송하는 클래스다.
테스트할 때마다 이메일이 날아가면 곤란하다.
그래서 가짜 구현체(FakeEmailSender)를 만들었다고 해도, 현재 구조에서는 끼워 넣을 방법이 없다.

public class FakeEmailSender extends EmailSender {
    @Override
    public void send(String message) {
        System.out.println("(가짜) 이메일 전송: " + message);
    }
}

OrderService 생성자 안에 new EmailSender()가 고정되어 있기 때문에,
FakeEmailSender를 사용하려면 소스 코드를 직접 수정해야 한다.

문제 2: 결합도가 높다

OrderService의 역할은 주문 처리다.
그런데 지금은 EmailSender 객체를 직접 생성하는 책임까지 지고 있다.

OrderService가 변경 없이 할 수 없는 것들:

  • EmailSenderSmsSender로 교체
  • 테스트용 FakeEmailSender로 교체
  • 성능 측정용 LoggingEmailSender로 교체

교체할 때마다 OrderService 코드를 열어야 한다.
이것이 결합도가 높다는 의미다.

문제 3: 복잡도 폭발

실제 서비스 규모에서는 이런 상황이 온다.

OrderService orderService = new OrderService(
    new EmailSender(
        new SmtpConnection(
            new NetworkConfig(...)
        )
    )
);

클래스가 수십, 수백 개로 늘어나면 개발자가 이걸 직접 관리하는 건 사실상 불가능하다.
누락이 생기고, 실수가 생기고, 유지보수가 지옥이 된다.


2. 해결책: 의존성 주입 (DI, Dependency Injection)

문제의 핵심은 OrderServiceEmailSender직접 만든다는 것이다.

해결책은 간단하다. 직접 만들지 말고, 외부에서 받자.

// 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), 의존성 주입이다.

객체가 필요로 하는 의존성을 직접 생성하지 않고, 외부에서 주입받는 것


3. 그래서 누가 주입해주나: IoC Container

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
객체를 누가 만드나개발자가 직접 newContainer가 대신
의존성을 누가 연결하나개발자가 직접 주입Container가 자동으로
제어권이 어디에개발자Framework

객체 생성과 관리의 제어권이 개발자에서 Framework(Spring)로 넘어간 것,
이것이 "제어의 역전"이라는 이름의 유래다.


4. Spring에서 실제로 어떻게 동작하나

@Component: "Spring아, 이 클래스 관리해줘"

@Component
public class EmailSender {
    public void send(String message) {
        // 이메일 발송 로직
    }
}

@Component를 붙이면 Spring이 앱 시작 시 이 클래스를 new해서 보관한다.
이렇게 Spring Container에 보관된 객체를 Bean이라고 부른다.

@Autowired: "Spring아, 보관중인 거 여기에 넣어줘"

@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을 꺼내서 생성자에 주입해준다.

Spring이 처리하는 순서

앱 시작
  ↓
@Component 붙은 클래스 스캔
  ↓
EmailSender 발견 → new EmailSender() 후 보관 (Bean 등록)
  ↓
OrderService 발견 → 생성자에 EmailSender 필요 확인
  ↓
보관중인 EmailSender를 꺼내서 new OrderService(emailSender) 완료
  ↓
개발자는 그냥 OrderService 가져다 씀

개발자가 new를 한 줄도 쓰지 않았다.


5. 전체 흐름: Controller까지 확장

실제 웹 요청 처리 흐름을 보면:

@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() 호출

각 클래스는 바로 아래 계층만 알면 된다.
OrderControllerOrderService만, OrderServiceEmailSender만.


6. 핵심 개념 정리

개념한 줄 정의
DI (Dependency Injection)객체가 필요한 의존성을 외부에서 주입받는 것
IoC (Inversion of Control)객체 생성/관리 제어권이 개발자에서 Framework로 넘어가는 것
BeanSpring Container가 생성하고 관리하는 객체
@Component"이 클래스를 Bean으로 등록해줘"라고 Spring에게 알리는 어노테이션
@Autowired"Container에서 해당 Bean을 꺼내 여기에 주입해줘"라는 어노테이션

DI와 IoC의 관계

DI는 방법이고, IoC는 목표다.
DI라는 방법으로 IoC라는 목표를 달성한다.


마치며

처음 Spring을 접하면 @Component@Autowired가 마법처럼 느껴질 수 있다.
하지만 그 배경에는 명확한 문제의식이 있다.

  • 높은 결합도 문제 → DI로 해결
  • 수백 개 객체를 사람이 관리하는 한계 → IoC Container로 해결

Spring은 이 두 가지를 프레임워크 레벨에서 자동화해준다.
개발자는 비즈니스 로직에만 집중할 수 있게 된다.

profile
Backend Developer | Aspiring Full-Stack Enthusiast

0개의 댓글