Spring이 제공하는 IoC와 DI는 단순한 기능이 아니다

박성용·2025년 3월 28일

스프링, 너 뭔데?

목록 보기
1/1

개요

Spring Framework를 처음 공부할 때 "Spring은 IoC와 DI를 제공한다"는 개념을 들으면서 시작하실 겁니다. 저도 처음에는 IoC는 뭐고, DI는 뭐고, IoC Container는 뭔지 스스로 잘 정리했다고 생각하고 넘어갔는데 나중에 막상 IoC가 무엇인지 DI가 무엇인지 얘기해보라고 하면 당당하게 정돈된 내용을 얘기하지 못하는 저의 모습을 발견할 수 있었습니다.

그래서 IoC가 무엇이고 DI는 무엇인지에 대해 제대로 알아야겠단 생각을 하게 되면서 이번에 깊이 공부를 하게되었고 결국엔 특히 DI 개념이 객체지향 설계의 철학을 실현하는 방식이라는 것을 깨달았고 이것이 결국엔 SOLID 원칙의 OCP나 DIP와 같은 개념과 맞닿아 있는 기술이라는 것을 깨달으면서 Spring은 정말 잘 만든 프레임워크라는 것을 다시 한번 느끼게 되었습니다.

정리하자면 이번 글에서는 Spring이 제공하는 IoC와 DI가 무엇인지 알아보고 DI가 어떻게 SOLID 원칙 중 DIP를 어떻게 실현하는지에 대해 정리해보겠습니다.

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

IoC는 "제어의 역전"이라는 뜻입니다. 전통적인 절차적 프로그래밍에서는 객체를 생성하고 연결하고 실행하는 제어권이 개발자에게 있었습니다. 하지만 IoC는 이 제어권을 프레임워크(즉, Spring)에게 넘겨주는 개념입니다.

기존 방식

// 기존 방식
UserService service = new UserService(new UserRepository());

IoC 방식

// IoC 방식 (Spring이 객체를 생성하고 주입)
@Autowired
private UserService userService;

IoC Container란?

Spring에서는 이 IoC 개념을 ApplicationContext라는 이름의 IoC 컨테이너를 통해 실현합니다. 이 컨테이너는 단순히 객체를 만들어주는 공장이 아닙니다. 객체의 생명주기를 관리하고, 필요한 시점에 의존성을 자동으로 주입해주는 역할을 합니다.

IoC에 대한 개념을 처음 접하게 되면 이런 생각이 드실 겁니다. "그렇다면 개발자는 뭘 해야 돼...?" 개발자는 IoC Container에게 2가지 정보를 제공하면 됩니다.

  1. POJO (Plain Old Java Object)
  • 순수한 자바 객체입니다. 특정 인터페이스나 클래스를 상속하면 안됩니다.
  • 우리가 작성한 비즈니스 로직의 주체입니다.
  1. Configuration Metadata (설정 정보)
  • POJO를 어떻게 생성하고, 어떻게 연결하고, 어떤 시점에 사용할 것인지에 대한 전략 및 설정에 대한 부분입니다.
  • 대표적인 설정 방법은 다음과 같습니다
    • 어노테이션 기반: @Component, @Service, @Repository, @Configuration, @Bean
    • XML 설정
    • 자바 코드 기반 설정 (JavaConfig)

즉, 개발자는 객체를 생성하고 의존관계를 설정하는 코드를 직접 작성하지 않고 단지 필요한 객체(POJO)구성 정보(Bean 설정 또는 어노테이션)만 Spring에게 넘깁니다. 그리고 나머지는 Spring이 알아서 처리합니다.

이로 인해 개발자는 객체의 생성과 연결 방식에 신경 쓰지 않고 순수 비즈니스 로직에만 집중할 수 있게 됩니다.

이에 대한 자세한 설명을 알고싶으신 분들은 Spring 공식문서에서 확인하시는 것을 강력하게 권장드립니다.

DI(Dependency injection) - 객체 간의 결합을 약하게 만드는 기술

DI는 객체가 자신이 사용할 객체를 직접 생성하지 않고 외부로부터 주입받는 방식을 의미합니다.

아래의 두개의 예시는 결합도가 높은 설계와 결합도가 낮은 설계에 대한 예시를 보여주고 있습니다.

예시 1 : 결합도가 높은 설계(안 좋은 예)

public class OrderService {
    private final UserRepository userRepository = new MemoryUserRepository(); // 강한 결합
}

예시2: 결합도가 낮은 설계(좋은 예)

public class OrderService {
    private final UserRepository userRepository;

    // 생성자 주입
    public OrderService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

둘의 차이가 보이시나요? 안 좋은 예의 경우엔 클래스 내부에서 필요한 개체를 직접 생성하고 있고 좋은 예의 경우엔 클래스 내부에서 개체를 직접 생성하는 방식이 아닌 외부에서 전달받고 있습니다.

즉, DI는 필요한 개체간 협력 방식을 설계하는 전략이고 필요한 개체가 어떤 구체화된 개체인지 알 필요 없이 전달받은 개체를 주입하여 사용할 수 있도록 하는 개념입니다.

더 나아가 Spring은 IoC 컨테이너를 활용해 이 주입을 자동화하는 하는 것이며 이것이 Spring 설계의 핵심입니다. Spring은 IOC 구현 기술로 DI를 사용하고 있다고 이해하는 것이 정확합니다.

개발자는 단지 필요한 추상화된 의존성만 선언하면 IoC 컨테이너가 알아서 객체의 생성 책임과 생명주기를 관리해주니 개발자는 비즈니스 로직에만 집중할 수 있게 되니 너무 편리하지 않나요?

DI와 DIP의 관계

DIP(Dependency Inversion Principle): 고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 추상화(인터페이스)에 의존해야 한다.

위의 내용은 SOLID 원칙 중 DIP의 개념에 대해 정리한 것입니다. 텍스트만 읽어보면 이해하기 쉽지 않습니다. 그런데 DI라는 방법을 통해 DIP가 적용되는 과정을 보시면 아 어떤 개념인지 곧바로 감을 잡으실 수 있으실 겁니다.

우선 상위 모듈과 하위 모듈을 정리해야 할 것 같습니다. 오해하시면 안되는 것이 상위와 하위라는 명칭에 의해 상위 모듈과 하위 모듈은 상속의 관계라고 혼동할 수 있는데 둘은 그런 관계가 아닙니다.

상위 모듈은 하위 모듈을 필요로 하는 개체입니다. 그리고 하위 모듈은 필요의 대상이 되는 개체입니다.

객체 지향 프로그래밍에선 모든 것이 개체이고 개체간의 연결관계를 설정할 때 상위 모듈에서 하위 모듈을 강하게 결합하고 있다면 유연성은 떨어지게 됩니다.

상위 모듈이 하위 모듈을 강하게 연결하는 예시는 다음과 같습니다.

class MechanicalKeyboard {
    public void type() {
        System.out.println("Typing on mechanical keyboard...");
    }
}

class Computer {
    private MechanicalKeyboard keyboard; // 구체적인 구현 클래스에 직접 의존

    public Computer() {
        this.keyboard = new MechanicalKeyboard(); // 직접 객체 생성
    }

    public void useKeyboard() {
        keyboard.type();
    }
}

위의 예시에서 Computer 클래스는 MechanicalKeyboard를 필요로 하고 있습니다. 즉, Computer 클래스는 상위 모듈이고 MechanicalKeyboard는 하위 모듈입니다. 그런데 Computer 클래스의 내부 구조를 보니 생성자 함수 내부에서 MechanicalKeyboard를 생성하여 필드값으로 저장하고 메서드 내부에서 해당 객체를 사용하고 있는 것을 확인할 수 있습니다.

이러한 상황에서 만약에 MechanicalKeyboard가 아니라WirelessKeyboard를 필요로 하게 되면 어떻게 될까요?
해당 구현체에 맞게 또 코드를 수정해야 되는 문제가 생깁니다. OCP(개방-폐쇄의 원칙)을 위반하게 됩니다.
위의 예시로만 보면 겨우 몇개의 코드만 수정하면 되지만 프로젝트 단위가 커지면 커질수록 이 문제는 심각해질 것입니다.

그렇다면 어떻게 이 문제를 해결할 수 있을까요?
정답은 결합도를 느슨하게 해주는 것입니다. 너무 말이 추상적이죠? 이것을 구체적으로 가능하게 하는 방법중 하나가 DI인 것입니다. 즉, DI는 DIP를 가능하게 하는 방법중 하나인 것입니다.

위에서 DI는 뭐라고 했죠? DI는 내부에서 자원을 생성하는 것이 아니라 외부에서 자원을 가져오는 것입니다. DI를 적용한 결합도를 느슨하게 한 예시는 다음과 같습니다.

// 추상화된 인터페이스
interface Keyboard {
    void type();
}

// 다양한 키보드 구현체
class MechanicalKeyboard implements Keyboard {
    public void type() {
        System.out.println("Typing on mechanical keyboard...");
    }
}

class WirelessKeyboard implements Keyboard {
    public void type() {
        System.out.println("Typing on wireless keyboard...");
    }
}

// DIP 적용: 인터페이스에 의존
class Computer {
    private Keyboard keyboard;

    // 의존성 주입 (Dependency Injection)
    public Computer(Keyboard keyboard) {
        this.keyboard = keyboard;
    }

    public void useKeyboard() {
        keyboard.type();
    }
}

public class DIPExample {
    public static void main(String[] args) {
        Keyboard mechanical = new MechanicalKeyboard();
        Computer pc1 = new Computer(mechanical);
        pc1.useKeyboard();

        Keyboard wireless = new WirelessKeyboard();
        Computer pc2 = new Computer(wireless);
        pc2.useKeyboard();
    }
}

위의 예시를 확인해보시면 이제 더이상 ComputerMechanicalKeyboard라는 구체화된 클래스에 의존적이지 않아도 됩니다. Computer는 이제 Keyboard라는 인터페이스에 의존적이게 되면서 해당 인터페이스를 구현하는 어떤 구현체든 상관없이 모두 가져와서 사용할 수 있게 됩니다.

즉, 상위 모듈은 DI 방법을 통해 하위 모듈이 아니라 인터페이스에 의존하게 되면서 인터페이스에만 집중해서 이 인터페이스에서 정의한 코드만 사용할거고 이것만 필요하고 이거를 누가 어떻게 구현했는지는 상관을 전혀 안하고 클래스를 설계할 수 있게 됩니다.

정리

"IoC는 DI를 자동화하고 DI는 DIP를 구현한다. "

위의 내용들을 토대로 IoC와 DI, 그리고 결론적으론 DIP 구현에 대한 개념이 유기적으로 연결된다는 것을 확인해볼 수 있었고 결론적으로 Spring은 우리가 객체지향적으로 더 잘 설계할 수 있도록 도와주는, 훌륭한 설계 보조자라는 것을 다시한번 깨달으며 앞으로 Spring의 구조를 더 면밀하게 분석해보면서 객체 지향에 대한 전반적인 개념을 모두 정리하는 것이 저의 목표입니다.

긴 글 읽어주셔서 감사드리고 오늘 하루도 수고 많으셨습니다

profile
지속 가능한 개발과 꾸준한 성장을 목표로 하는 Web Developer 박성용입니다

0개의 댓글