[Spring] 스프링 IoC(제어의 역전), DI(의존성 주입) 완벽 이해하기

스프링을 처음 접하면 만나게 개념이 바로 DI(Dependency Injection, 의존성 주입)와 IoC(Inversion of Control, 제어의 역전)입니다. 이 제어의 역전(IoC)과 의존성 주입(DI)은 객체지향 프로그래밍에서 코드의 유연성과 유지보수성을 높이는 데 중요한 설계 패턴으로 한번 구조를 정확히 이해하면 유지보수와 확장성 면에서 큰 장점을 얻을 수 있습니다. 오늘은 Spring DI와 IoC를 쉽게 정리해보려고 합니다.

 

IoC(Inversion of Control)란 무엇인가?

전통적인 자바 애플리케이션에서는 객체가 스스로 의존하는 객체를 만들고 제어했습니다. 예를 들어 서비스가 레포지토리를 직접 생성하는 방식이죠.

public class MemberService {
    private MemberRepository memberRepository = new MemberRepository();
}

자바 개발을 하다보면 이렇게 클래스를 생성할때 new로 객체를 직접 생성하는 코드를 보신적이 있으실거에요. 여기서는 객체를 생성하는 제어권(control)이 개발자, 혹은 코드 내부에 있습니다. 하지만 보통 스프링에서는 이렇게 사용하지 않습니다.

 

스프링에서는 이런 식으로 객체를 직접 생성하지 않고, 객체 생성과 의존 관계 주입을 "스프링 컨테이너에게 맡기는 방식"을 사용합니다. 즉, 개발자가 `new`로 일일이 만들고 연결하던 일을 스프링이 대신 해주는 거죠. 우리는 “어떤 객체가 어떤 걸 필요로 한다”는 관계(의존성)만 선언해두고, 실제 생성과 주입은 스프링이 알아서 처리합니다.

 

IoC, DI 의 장점

  • 테스트 용이성 증가 : 의존 객체를 외부에서 주입받기 때문에, Mock 객체로 쉽게 교체할 수 있습니다.
  • 확장성과 유연성 향상 : 내부 로직 변경 없이도 구현체만 바꾸면 구조 전체를 움직일 수 있습니다. (예: MemoryRepository → JpaRepository 변경)
  • 결합도 감소, 재사용성 증가 : 객체 간 의존 관계가 명확히 분리되어 관리되므로 유지보수가 훨씬 수월해집니다.

 

IoC, DI 구조

이 그림은 Spring의 핵심 철학인 IoC(Inversion of Control) 아래에서 DI(Dependency Injection)와 DL(Dependency Lookup)이 각각 무엇을 의미하는지 정리해 놓은 구조입니다.

 

IoC(Inversion of Control) – 제어의 역전

가장 왼쪽의 빨간색 IoC는 전체 개념의 큰 틀입니다. IoC란 "누가 객체를 만들고 연결할 것인가?"에 대한 제어권을 개발자가 아닌 스프링에게 넘기는 것을 의미합니다. 쉽게 말해서,

  • 객체 생성
  • 객체 간 의존 관계 설정
  • 라이프사이클 관리

이 모든 것을 개발자가 직접 하지 않고, 스프링 컨테이너가 대신 관리한다는 뜻이에요.

 

위 그림의 초록색 원 "DL"은 예전에 사용되던 방식으로, 필요한 객체를 개발자가 직접 컨테이너에게 “가져오는” 방식이고, 위 그림의 보라색 DI는 오늘날 스프링에서 기본적으로 사용하는 방식입니다.

 

DI(Dependency Injection) — 의존성 주입

DI는 개발자가 컨테이너에게 필요한 객체를 “요청”하지 않고, 스프링이 필요한 객체를 알아서 "주입" 해주는 방식입니다. 즉, Service는 Repository가 필요하다는 선언만 하고 실제로 Repository 객체를 넣어주는 작업은 스프링이 담당합니다 그래서 DL보다 훨씬 깔끔하고 유지보수가 좋아 현대 스프링의 핵심 방식이 되었어요. 스프링뿐 아니라 PicoContainer 등 다른 프레임워크도 DI 방식을 사용합니다.

 

 DI를 적용하는 세 가지 방식 

생성자 주입 - Constructor Injection (가장 권장)

생성자를 통해 필요한 객체를 주입받는 방식입니다.

@Service
public class MemberService {

    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

불변성 보장, NPE 방지, 테스트 용이 등 다양한 이유로 가장 많이 사용되는 방식입니다.

 

세터 주입 - Setter Injection

세터 메서드를 통해 의존성을 주입하는 방식입니다.

@Autowired
public void setRepo(MemberRepository repo) {
    this.repo = repo;
}

유연하지만, 런타임 시점에 객체가 변경될 가능성이 생겨 자주 사용되지는 않습니다.

 

메서드 주입 - Method Injection

Method Injection은 특정 메서드가 호출될 때 그 메서드의 파라미터로 의존성을 주입받는 방식입니다.

@Component
public abstract class CommandManager {

    // 매번 새로운 Command 빈이 필요할 때
    public void process() {
        Command command = createCommand();  // 여기서 DI 발생!
        command.execute();
    }

    @Lookup
    protected abstract Command createCommand();
}

이 방식은 프로토타입 빈(prototype bean)을 매번 새로 만들어야 할 때 자주 쓰입니다.