Programming/Spring

Spring 의존성 주입

khj1999 2025. 2. 6. 21:00

🚀 의존성 주입(Dependency Injection, DI)을 하는 이유

스프링에서 의존성 주입(DI) 을 사용하는 이유는 객체 간의 결합도를 낮추고 유지보수성을 높이기 위해서야. DI를 사용하면 코드가 더 유연하고 확장 가능해진다.


1. 객체 간의 결합도(Dependency)를 낮추기 위해

  • DI를 사용하면 객체 간의 강한 결합도를 줄이고 느슨한 결합(Loosely Coupled)을 유지할 수 있다.
  • 예를 들어, 아래처럼 직접 객체를 생성하면 Car 클래스는 Engine 구현체에 강하게 의존하게 됨.

의존성 주입 없이 직접 객체 생성 (강한 결합)

public class Car {
    private Engine engine = new Engine();  // 직접 객체 생성

    public void start() {
        engine.run();
    }
}
  • 만약 Engine을 ElectricEngine으로 바꾸려면 Car 코드를 수정해야 한다!
  • 코드 수정 없이 쉽게 변경할 수 있도록 DI를 적용하는 게 좋음.

의존성 주입을 활용한 코드 (느슨한 결합)

public class Car {
    private final Engine engine;

    public Car(Engine engine) {  // 생성자를 통한 DI
        this.engine = engine;
    }

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

이제 Car 클래스는 특정 엔진 구현체에 의존하지 않으며, DI를 통해 ElectricEngine을 쉽게 주입할 수 있다.

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

2. 객체의 재사용성과 확장성을 높이기 위해

  • DI를 사용하면 객체를 쉽게 교체할 수 있어서 재사용성이 좋아짐.
  • 인터페이스 기반 프로그래밍이 가능하므로 새로운 구현체를 추가하기 쉬움.

예를 들어, Engine 인터페이스를 사용하면 Car 클래스의 코드 변경 없이 다른 엔진으로 교체 가능하다

public interface Engine {
    void run();
}

public class GasolineEngine implements Engine {
    public void run() {
        System.out.println("Gasoline engine running!");
    }
}

public class ElectricEngine implements Engine {
    public void run() {
        System.out.println("Electric engine running!");
    }
}

이제 Car는 다양한 엔진을 주입받을 수 있다.

Car car1 = new Car(new GasolineEngine());  // 휘발유 엔진
Car car2 = new Car(new ElectricEngine());  // 전기 엔진

3. 코드의 유지보수성을 높이기 위해

  • 의존성을 직접 관리하는 대신 스프링 컨테이너가 주입해 주므로 객체 생성을 신경 쓰지 않아도 됨.
  • 예를 들어, @Autowired를 사용하면 자동으로 객체가 주입됨.
@Component
public class Car {
    private final Engine engine;

    @Autowired
    public Car(Engine engine) {
        this.engine = engine;
    }
}
  • 만약 Engine 구현체가 바뀌어도 Car 코드를 수정할 필요 없이 스프링이 알아서 주입해 줌!

4. 테스트가 쉬워짐 (유닛 테스트 가능)

  • DI를 사용하면 테스트 시 Mock 객체를 주입할 수 있어서 단위 테스트가 훨씬 쉬워짐.
@Test
void carShouldStartWithMockEngine() {
    Engine mockEngine = mock(Engine.class);  // 가짜 객체(Mock) 생성
    Car car = new Car(mockEngine);

    car.start();

    verify(mockEngine).run();  // Mock 객체의 메서드 호출 검증
}
  • 직접 객체를 생성하면 테스트할 때 원하는 객체로 대체하기 어렵지만, DI를 사용하면 가짜 객체(Mock)를 쉽게 넣을 수 있음.

5. 스프링 컨테이너의 관리 덕분에 더 효율적인 리소스 사용 가능

  • DI를 사용하면 객체를 직접 생성하는 대신 스프링 컨테이너가 싱글톤으로 관리함.
  • 따라서 불필요한 객체 생성을 줄이고 메모리를 효율적으로 사용할 수 있음.
@Component
public class Engine {
    public Engine() {
        System.out.println("Engine 객체 생성됨!");
    }
}
  • 스프링 컨테이너가 알아서 객체를 한 번만 생성하고 필요한 곳에 주입함.

📌 결론: DI는 유지보수성과 확장성을 높이고, 객체 재사용성을 극대화함

DI를 사용하는 이유 이점

결합도를 낮춤 특정 구현체에 종속되지 않고 쉽게 변경 가능
재사용성 증가 객체를 교체하거나 확장하기 쉬움
유지보수성 향상 코드 수정 없이 의존성 변경 가능
테스트 용이 Mock 객체를 활용한 유닛 테스트 가능
리소스 효율성 증가 스프링 컨테이너가 객체를 효율적으로 관리

🔥 즉, DI를 사용하면 코드가 더 유연하고 유지보수하기 쉬워진다! 💡


의존성 주입의 방식

자바 스프링의 의존성 주입(Dependency Injection, DI) 방식은 크게 세 가지로 나눌 수 있다.


1. 생성자 주입 (Constructor Injection)

📌 개념

  • 생성자를 이용해 의존성을 주입하는 방식
  • 주입받을 객체를 생성자의 매개변수로 전달

예제

@Component
public class Car {
    private final Engine engine;

    @Autowired  // 스프링 4.3부터는 생략 가능
    public Car(Engine engine) {
        this.engine = engine;
    }
}

🏆 장점

  • 불변성 유지: final 키워드를 사용하여 불변 객체를 만들 수 있음
  • 순환 참조 방지: 생성자 주입 시 순환 참조가 발생하면 스프링이 애플리케이션 실행 시점에서 오류를 감지
  • 테스트 용이: 객체를 명확하게 주입하기 때문에 테스트 시 명시적인 의존성 주입 가능

단점

  • 필요 없는 의존성까지 강제할 가능성이 있음
  • 의존성이 많을 경우 생성자 코드가 길어질 수 있음

2. 필드 주입 (Field Injection)

📌 개념

  • 필드에 직접 @Autowired 어노테이션을 붙여 주입하는 방식

예제

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

🏆 장점

  • 코드가 간결하며, 작성이 쉽고 직관적임
  • 별도의 생성자나 Setter가 필요 없음

단점

  • 의존성 변경이 어려움 (Setter가 없기 때문에 테스트에서 Mock 객체를 주입하기 어려움)
  • 순환 참조 문제를 런타임에서 발견 (컴파일 시점이 아닌 실행 시점에서 오류 발생)
  • 객체가 스프링 컨테이너에 종속적 (@Autowired가 스프링에서만 동작하므로, 스프링 없이 테스트하기 어려움)

⚠ ⚠ 스프링 공식 문서에서도 필드 주입보다는 생성자 주입을 권장함


3. Setter 주입 (Setter Injection)

📌 개념

  • Setter 메서드를 통해 의존성을 주입하는 방식

예제

@Component
public class Car {
    private Engine engine;

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

🏆 장점

  • 선택적 의존성 주입 가능 (@Autowired(required = false) 사용 가능)
  • 유연한 변경 가능 (런타임에서 특정 의존성을 변경할 수 있음)

단점

  • 불변성을 유지할 수 없음 (Setter를 통해 언제든 변경 가능)
  • 객체가 완전히 생성된 후에야 의존성이 주입됨 (객체 생성 시점과 의존성 주입 시점이 다름)

어떤 방식이 가장 좋을까?

의존성 주입 방식 장점 단점 권장 여부

생성자 주입 불변성 유지, 순환 참조 방지 생성자가 길어질 수 있음 ✅ 가장 권장됨
필드 주입 코드가 간결함 테스트 어려움, 순환 참조 문제 발생 가능 ❌ 비권장
Setter 주입 선택적 주입 가능, 유연함 불변성 유지 어려움 ⚠ 일부 경우에 사용 가능

🔥 결론: 기본적으로 생성자 주입을 사용하고, 선택적 의존성이 필요하면 Setter 주입을 활용하는 것이 좋음. 필드 주입은 추천되지 않음! 🚀


추가적으로 @RequiredArgsConstructor를 활용하면 생성자 주입을 더욱 편리하게 사용할 수 있다! 😃