7 분 소요

🔧 DI (Dependency Injection)란?

DI(의존성 주입)은 객체가 직접 필요한 의존 객체를 생성하지 않고, 외부에서 주입받는 방식


📌 예: “자동차(Car)”에 “엔진(Engine)”이 필요할 때

❌ 직접 생성 (의존성 직접 관리)

class Car {
    private Engine engine = new Engine(); 
    // Car가 직접 Engine을 생성함
}

✅ 의존성 주입 방식

class Car {
    private Engine engine;

    public Car(Engine engine) {  // 외부에서 Engine을 주입받음
        this.engine = engine;
    }
}

이렇게 하면 Car는 어떤 종류의 엔진이 들어오는지 몰라도 되고, 테스트도 더 유연하게 할 수 있다.

🧩 주요 용어 정리

용어 설명
Dependency (의존성) 한 객체가 다른 객체에 기능적으로 의존하고 있을 때, 그 다른 객체를 “의존성”이라고 부름. 예: Car → Engine
Injection (주입) 의존 객체를 직접 생성하지 않고, 외부에서 넣어주는 행위
DI Container 의존성들을 관리하고 주입해주는 컨테이너. Spring의 ApplicationContext가 대표적
Constructor Injection 생성자를 통해 의존성을 주입 (가장 권장되는 방식)
Setter Injection setter 메서드를 통해 의존성 주입
Field Injection 필드에 직접 주입 (@Autowired 등). 간단하지만 테스트 어려움
IoC (Inversion of Control) “제어의 역전” — 객체의 생성과 생명주기 관리를 개발자가 아니라 프레임워크가 담당하는 개념. DI는 IoC를 구현하는 한 방식

🏗️ DI를 사용하는 이유

  • 결합도 낮춤 → 코드가 더 유연하고 확장 가능
  • 테스트 쉬움 → 가짜(Mock) 객체 주입해서 단위 테스트 가능
  • 유지보수 편리 → 새로운 의존성으로 쉽게 교체 가능
  • 관심사 분리 → 객체는 자신의 로직에만 집중하고 의존성 관리는 외부에 맡김

🔁 자주 함께 언급되는 개념

개념 설명
Service, Repository 등 역할 분리 DI를 통해 각 계층 간 결합을 느슨하게 만들 수 있음
@Component, @Service, @Repository Spring이 자동으로 Bean으로 등록해서 DI 가능하게 함
@Autowired, @Inject, @Qualifier 의존성을 주입하는 방법과 조건을 지정함

🔧 Spring에서의 DI 예제

🎯 목표

  • CarEngine에 의존
  • 다양한 방식으로 Engine을 주입 (DI)
  • DI를 통해 Car는 어떤 Engine이든 받아서 쓸 수 있도록 설계

🔹 인터페이스 및 구현체

public interface Engine {
    void start();
}
@Component
public class GasEngine implements Engine {
    @Override
    public void start() {
        System.out.println("Gas engine starting...");
    }
}
@Component
public class ElectricEngine implements Engine {
    @Override
    public void start() {
        System.out.println("Electric engine starting...");
    }
}

🔹 Car 클래스 (DI 적용)

방법 1: 생성자 주입 (가장 권장됨)

@Component
public class Car {

    private final Engine engine;

    @Autowired  // 생략 가능 (Spring 4.3 이상)
    public Car(Engine engine) {
        this.engine = engine;
    }

    public void drive() {
        engine.start();
        System.out.println("Car is moving");
    }
}

방법 2: 필드 주입 (테스트 불편, 비권장)

@Component
public class Car {

    @Autowired
    private Engine engine;

    public void drive() {
        engine.start();
        System.out.println("Car is moving");
    }
}

방법 3: Setter 주입 (선택적 의존성에 적합)

@Component
public class Car {

    private Engine engine;

    @Autowired
    public void setEngine(Engine engine) {
        this.engine = engine;
    }

    public void drive() {
        engine.start();
        System.out.println("Car is moving");
    }
}

🔧 필드 주입이 테스트에서 불편한 이유

❗ 문제: 주입을 직접 할 수 없다

필드 주입은 아래처럼 private 필드에 @Autowired를 붙여 자동 주입

@Component
public class Car {
    @Autowired
    private Engine engine;

    public void drive() {
        engine.start();
    }
}

이런 경우 테스트 코드에서 직접 engine을 주입할 수 없다

  • Car car = new Car(); 로 생성해도 enginenull
  • engine을 주입하려면 리플렉션을 써야 함:
@Test
void testDrive_withReflection() throws Exception {
    Car car = new Car();
    Engine mockEngine = mock(Engine.class);

    Field field = Car.class.getDeclaredField("engine");
    field.setAccessible(true);
    field.set(car, mockEngine);  // 강제로 주입
    car.drive();
}

✅ 생성자 주입과 비교

생성자 주입이면, 다음과 같이 쉽게 테스트 가능:

@Test
void testDrive() {
    Engine mockEngine = mock(Engine.class);
    Car car = new Car(mockEngine);  // 직접 주입 가능

    car.drive();

    verify(mockEngine).start();  // 검증도 가능
}

💡 필드 주입의 다른 단점

항목 내용
❌ 불변성 없음 생성 시점에 주입되지 않아서 final 키워드 못 씀
❌ 명시적 의존성 없음 어떤 의존성이 필요한지 생성자 시그니처로는 알 수 없음
❌ 테스트 어려움 위에서 본 것처럼 mock 주입, 의존성 교체가 어렵고 우회적
❌ 유지보수 어려움 코드만 보고는 어떤 객체가 들어오는지 추론이 어려움
   

✅ 정리

  • 필드 주입은 편리해 보이지만, 유닛 테스트, 불변성, 명시성 측면에서 모두 불리함
  • 그래서 Spring 공식 가이드, Josh Long 같은 전문가들도 “항상 생성자 주입을 쓰라”고 권장
  • 테스트에서는 특히 mock 객체를 주입하기 어려워서 정말 불편함

✅ Setter 주입이 “선택적 의존성”에 적합한 이유

어떤 의존성은 있으면 좋지만, 없어도 동작이 가능한 경우 → 꼭 생성자에 넣을 필요가 없음
→ 그래서 setter로 필요할 때만 주입하도록 설계

@Component
public class MyService {

    private Logger logger;

    @Autowired(required = false)  // 주입 안 돼도 오류 안 남
    public void setLogger(Logger logger) {
        this.logger = logger;
    }

    public void doSomething() {
        if (logger != null) {
            logger.log("Starting...");
        }

        System.out.println("Doing something important!");
    }
}
  • Logger없어도 로직은 돌아감
  • 있을 경우에만 로깅 수행
  • 따라서 Setter 주입이 적합
✅ Setter 주입의 장점
항목 설명
✔ 선택적 의존성 표현 가능 @Autowired(required = false)있으면 주입, 없으면 넘어감
✔ 나중에 바꿀 수 있음 Bean 생성 이후 다른 객체로도 주입 가능 (동적으로 바꾸고 싶을 때)
✔ 테스트에서 mock 쉽게 주입 가능 setLogger(mockLogger)처럼 주입 가능
❌ Setter 주입의 단점
항목 설명
❌ 불변 객체 설계 불가 setter는 값 변경 허용 → 상태가 바뀔 수 있음
❌ 의존성 누락 방지 어려움 주입을 깜빡해도 컴파일러가 안 잡음 (런타임 오류 유발 가능)
❌ 명시적 의존성 표현 약함 클래스만 보고는 필수인지 선택인지 바로 파악 어려움

✅ 비교 요약

구분 생성자 주입 Setter 주입 필드 주입
📌 용도 필수 의존성 선택적 의존성 가볍게 쓰기 쉬움 (but 비권장)
🔒 불변성 유지 가능 유지 불가 유지 불가
🔍 테스트 용이성 매우 좋음 좋음 나쁨
🧩 명시성 높음 낮음 낮음

🔚 정리

  • Setter 주입은 “필수는 아닌, 옵션적인” 의존성을 주입할 때 사용
  • 그 외에는 대부분 생성자 주입이 정석
  • Spring에서는 setter 주입도 가능하지만, 무분별하게 사용하면 **설계가 모호해지고 유지보수 어려워짐

🔧 2. DIP (Dependency Inversion Principle)

📘 DIP란?

의존성 역전 원칙:
“상위 모듈(비즈니스 로직)은 하위 모듈(구현)에 의존해서는 안 되며,
둘 다 추상(인터페이스)에 의존해야 한다.”

❌ DIP 위반 예

public class Car {
    private GasEngine engine = new GasEngine();  
    // 구현체에 의존 (강한 결합)
}

CarGasEngine이라는 구체 클래스에 의존 → 확장성 낮고, 테스트 어려움

✅ DIP 만족 예

public class Car {
    private Engine engine;

    public Car(Engine engine) {  // 인터페이스(추상)에 의존
        this.engine = engine;
    }
}

CarEngine이라는 추상 타입에만 의존 → 유연하고 테스트 용이

✅ 1. Engine 추상화(인터페이스) 정의
public interface Engine {
    void start();
}
  • Engine은 인터페이스이므로 “행위(기능)”만 정의
  • Car는 이제 이 추상 타입만 알면 됨
✅ 2. GasEngine 구현체 정의 (DIP 만족)
public class GasEngine implements Engine {
    @Override
    public void start() {
        System.out.println("Gas engine started.");
    }
}
  • GasEngineEngine 인터페이스를 구현한 클래스
  • 이 구현체는 실제 기능을 수행하는 클래스
✅ 3. Car 클래스는 추상화에만 의존
public class Car {
    private final Engine engine;

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

    public void drive() {
        engine.start();
    }
}

  • 이제 Car는 더 이상 GasEngine이 뭔지 몰라도 됨
  • 오직 Engine 인터페이스가 제공하는 기능(start())만 사용
✅ 4. 사용하는 쪽에서 주입 (DI)
public class Main {
    public static void main(String[] args) {
        Engine engine = new GasEngine(); // 어떤 구현체를 쓸지는 여기서 결정
        Car car = new Car(engine);

        car.drive(); // "Gas engine started." 출력
    }
}

  • CarEngine 타입을 받아서 사용하므로 DIP 만족
  • 구현체(GasEngine, ElectricEngine 등)는 주입을 통해 교체 가능
✅ 테스트도 쉬워짐 (모의 객체)
public class MockEngine implements Engine {
    public boolean started = false;

    @Override
    public void start() {
        started = true;
    }
}
@Test
void testCarDrives() {
    MockEngine mock = new MockEngine();
    Car car = new Car(mock);

    car.drive();

    assertTrue(mock.started);
}

💡 핵심 요약

구분 DIP 위반 DIP 만족
의존성 GasEngine 직접 생성 Engine 인터페이스 주입
결합도 높음 (변경 어려움) 낮음 (유연함)
테스트 어려움 (Mock 불가) 쉬움 (인터페이스로 대체 가능)
확장성 GasEngine만 가능 ElectricEngine 등 다른 엔진도 가능

🔁 DIP vs DI 차이

개념 설명
DIP (원칙) 설계 원칙. “추상화에 의존하라, 구현에 의존하지 마라.”
DI (기술) 구현 기술. “의존 객체를 외부에서 주입해라.” (이 원칙을 실현하는 방법 중 하나)

즉, DI는 DIP를 구현하기 위한 수단 중 하나다.

✅ DIP(의존성 역전 원칙) 핵심 정리

상위 모듈(정책)하위 모듈(구현)에 의존하지 말고,
둘 다 추상화(인터페이스)에 의존하라는 원칙.

📌 DIP를 제대로 이해하기 위해 꼭 알아야 할 추가 개념들

1. 상위/하위 모듈이 뭐냐?

  • 상위 모듈: 핵심 비즈니스 로직, 규칙, 정책 등 (예: OrderService)
  • 하위 모듈: 구체적인 구현 (예: MemoryOrderRepository, JdbcOrderRepository)
// DIP 위반 (상위가 하위에 직접 의존)
public class OrderService {
    private MemoryOrderRepository repository = new MemoryOrderRepository();
}
// DIP 만족 (상위도 하위도 인터페이스에 의존)
public class OrderService {
    private final OrderRepository repository;

    public OrderService(OrderRepository repository) {
        this.repository = repository;
    }
}

2. DIP를 적용하면 얻는 실질적 이점

이점 설명
✅ 확장성 새로운 구현체를 쉽게 붙일 수 있음 (RedisRepo, MongoRepo 등)
✅ 테스트 용이 FakeRepo, MockRepo를 바로 주입해서 테스트 가능
✅ 유지보수 편의 비즈니스 로직과 구현이 분리되어 변경 영향 ↓
✅ 의존 방향 명확 상위 정책이 구현 세부사항에 끌려가지 않음

3. DIP는 SRP(단일 책임 원칙)와 짝이다

  • DIP는 단일 책임 원칙(SRP)과 함께 “역할과 구현을 분리”하는 설계를 가능하게 해줘야함.
  • 예: OrderService는 “주문 처리”라는 역할만 집중하고, 저장 방식은 관심 없음.

4. DIP를 실현하는 방법은 DI

  • DIP는 설계 원칙이고,
  • DI(Dependency Injection)는 그걸 구현하는 기술/패턴
  • Spring은 DI를 통해 DIP를 자연스럽게 실현하게 해준다.

👉 DIP를 제대로 이해하려면:

  • 단순히 “인터페이스 써야 한다”는 수준을 넘어서서,
  • “의존 방향이 추상화로 향하도록 설계하는 것”이 핵심이고,
  • DI는 그걸 도와주는 기술이라는 점을 이해해야 한다.

카테고리:

업데이트:

댓글남기기