[JAVA] DI(의존성 주입)란 무엇인가?

[JAVA] DI(의존성 주입)란 무엇인가? (초보자 기준)

인터페이스와 다형성을 공부하다 보면 어디선가 꼭 등장하는 말이 있습니다. 바로 DI(Dependency Injection), 의존성 주입입니다.

처음 이 용어를 봤을 때 저는 "이건 스프링에서나 쓰는 거 아니야?" 라는 생각을 했습니다.

하지만 공부해보니, DI는 스프링 이전에 자바 개념으로 먼저 이해해야 할 개념이었습니다. 이번 글에서는 스프링 없이, 순수 자바 코드로 DI를 정리해보겠습니다.


1. 의존성이란?

DI를 이해하려면 먼저 의존성이라는 말부터 이해해야 합니다.

아래 코드를 보겠습니다.


class Engine {
    void start() {
        System.out.println("엔진이 동작합니다.");
    }
}

class Car {
    private Engine engine = new Engine();

    void run() {
        engine.start();
        System.out.println("자동차가 달립니다.");
    }
}

Car 클래스는 Engine 객체를 직접 생성해서 사용하고 있습니다. 이 경우 Car는 Engine에 의존한다고 말합니다.

문제는 Engine이 바뀌면 Car 코드도 함께 수정해야 한다는 점입니다.


2. 의존성을 직접 생성하면 생기는 문제

엔진이 하나만 있다면 문제가 없지만, 엔진 종류가 늘어나면 상황이 달라집니다.


class GasEngine {
    void start() {
        System.out.println("가솔린 엔진 시동");
    }
}

class ElectricEngine {
    void start() {
        System.out.println("전기 엔진 시동");
    }
}

Car 클래스 안에서 new GasEngine(), new ElectricEngine() 을 직접 바꾸는 구조라면, 엔진이 추가될 때마다 Car를 수정해야 합니다.

이 구조는 유지보수에 불리한 코드입니다.


3. 인터페이스로 의존성 분리하기

이 문제를 해결하기 위해 인터페이스를 사용합니다.


interface Engine {
    void start();
}

class GasEngine implements Engine {
    public void start() {
        System.out.println("가솔린 엔진 시동");
    }
}

class ElectricEngine implements Engine {
    public void start() {
        System.out.println("전기 엔진 시동");
    }
}

이제 Car는 구현체가 아니라 Engine 인터페이스에 의존하게 됩니다.


4. DI 적용 전 코드

인터페이스를 사용했지만, 아직 문제는 남아 있습니다.


class Car {
    private Engine engine = new GasEngine();

    void run() {
        engine.start();
        System.out.println("자동차가 달립니다.");
    }
}

여전히 Car 안에서 어떤 엔진을 쓸지 직접 결정하고 있습니다.


5. DI(의존성 주입)란?

DI는 객체가 사용할 의존성을 직접 생성하지 않고, 외부에서 주입받는 방식입니다.

즉, Car는 "엔진이 필요하다"라고만 말하고, "어떤 엔진인지는 외부에서 정해준다" 라는 구조입니다.


6. 생성자를 통한 의존성 주입

가장 기본적인 DI 방식은 생성자 주입입니다.


class Car {
    private Engine engine;

    Car(Engine engine) {
        this.engine = engine;
    }

    void run() {
        engine.start();
        System.out.println("자동차가 달립니다.");
    }
}

이제 실행 코드에서 어떤 엔진을 쓸지 결정합니다.


public class Main {
    public static void main(String[] args) {

        Engine engine = new GasEngine();
        Car car = new Car(engine);
        car.run();
    }
}

실행 결과 (Output)


가솔린 엔진 시동
자동차가 달립니다.

엔진을 바꾸고 싶다면 Car 코드는 수정할 필요가 없습니다.


Engine engine = new ElectricEngine();
Car car = new Car(engine);

7. DI의 장점

  • 객체 간 결합도가 낮아진다
  • 기존 코드를 수정하지 않고 확장 가능
  • 테스트 코드 작성이 쉬워진다

특히 테스트에서는 가짜 객체(Mock)를 주입할 수 있기 때문에 DI의 장점이 크게 느껴진다고 합니다.


8. 스프링과 DI의 관계

지금까지는 순수 자바 코드로 DI를 구현했습니다.

스프링은 이 과정을 프레임워크가 대신 관리해주는 역할을 합니다.

즉,

DI는 개념이고, 스프링은 DI를 편하게 해주는 도구다.

이렇게 이해하니 스프링에서 왜 인터페이스를 많이 쓰는지도 조금 이해가 되었습니다.


9. 정리

  • 의존성: 객체가 다른 객체를 사용하는 관계
  • DI: 의존성을 외부에서 주입하는 방식
  • 인터페이스 + 다형성이 DI의 핵심

처음에는 개념이 어렵게 느껴졌지만, 코드와 출력 결과를 직접 보면서 조금씩 이해할 수 있었습니다.

DI는 나중에 스프링을 공부할 때 반드시 다시 보게 되는 개념이기 때문에 지금 이렇게 정리해두면 도움이 많이 될 것 같습니다.