DI Dependency Injection 의존성 주입

김유진·2026년 3월 27일
post-thumbnail

📌 Spring 위주로 작성됨

개요

[!note]
객체가 필요로 하는 의존성을 외부에서 직접 주입받는 것을 DI 라고 한다

프로그래밍에서 구성 요소간의 의존관계가 소스코드 내부가 아닌 외부의 설정파일 등을 통해 정의되게 하는 디자인 패턴 중의 하나

[!note] 의존성이란?
의존성이라는 아주 멋들어지는 말을 써서 약간 헷갈릴 수 있는데,
A를 실행하기 위해 B를 호출하거나 기능을 빌려써야 한다. == A가 B에 의존한다.
그냥 필요하다는 말임

  • IoC 패턴 중 하나
  • 객체간 의존성을 낮춤[^1]
  • 외부에서 객체를 생성하고 전달

Dependency Inversion Principle (의존성 역전 원칙)

  • 상위 모듈이 하위 모듈[^2]에 의존관계를 가지지 않도록 구현해야 한다는 원칙.
  • 추상클래스는 그 구현체의 내용에 의존관계를 가지지 않는다.
  • 구현체가 추상클래스에 의존관계를 가질 수 있다.

[!note]

DIP: 의존성 역전 원칙의 핵심

1. 전통적인 의존 관계의 문제점

  • 상위 모듈이 하위 모듈의 구체적인 구현에 직접 의존함. [cite: 2026-01-20]
  • 하위 모듈 변경 시 상위 모듈까지 수정해야 하는 강한 결합이 발생함. [cite: 2026-01-20]

2. 역전(Inversion)의 메커니즘

  • 상위 모듈과 하위 모듈 사이에 추상화(인터페이스)를 둠. [cite: 2026-01-20]
  • 하위 모듈이 상위 모듈이 정의한 인터페이스에 의존하게 함으로써 의존 방향을 역전시킴. [cite: 2026-01-20]

3. 설계의 이점

  • 상위 모듈은 상세 구현의 변화로부터 독립됨. [cite: 2026-01-20]
  • 코드의 재사용성이 높아지고, 테스트 시 가짜 객체(Mock)를 주입하기 쉬워짐. [cite: 2026-01-20]

4. 요약

DIP는 상위 모듈이 하위 모듈을 무시하는 것이 아니라, 둘 다 추상화에 의존하게 하여 변화에 유연하게 대처하는 원칙임. [cite: 2026-01-20]

주입 방법

📌 @Autowired : 이 어노테이션을 달고 있다는 말은 "나 객체 필요해요" 하고 손들고 있는거랑 똑같음

종류

  • Constructor Injection
  • Setter Injection
  • Field Injection
  • @Configuration + @Bean

Constructor Injection

@Component
public class AppStartupRunner implements ApplicationRunner {

    private Greeting greeting;
    
    @Autowired
    public ApplicationRunner(Greeting greeting) {
        this.greeting = greeting;
    }

    @Override
    public void run(ApplicationArguments args) {
        greeting.sayHello();
    }
}
  • 말 그대로 생성자를 통해 사용
  • lombok이랑 연계해서 많이 사용됨
  • 이걸 권장함
  • 생성자가 하나면, @Autowired가 있다고 간주함

Setter Injection

@Component
public class AppStartupRunner implements ApplicationRunner {

    private Greeting greeting;

    @Autowired
    public void setGreeting(Greeting greeting) {
        this.greeting = greeting;
    }
    
    @Override
    public void run(ApplicationArguments args) {
        greeting.sayHello();
    }
}
  • setter에다가 주입
  • 근데 꼭 set~ 형태가 아니여도 됨, 다른 이름으로 해도 무방함

Field Injection

@Component
public class AppStartupRunner implements ApplicationRunner {

    @Autowired
    private Greeting greeting;


    @Override
    public void run(ApplicationArguments args) {
        greeting.sayHello();
    }
}
  • 명시적이지 않아서 그다지 권장하진 않는 듯

@Configuration + @Bean

@Configuration
public class GreetingConfig {

    @Bean
    American innerGreeting() {
        return new American();
    }
    // bean 으로 등록된 American 을 파라미터로 넣어준다
    @Bean
    Greeting englishGreeting(American american) {
        return new EnglishGreeting(american);
    }
}
  • 이러면 매개변수로 bean을 넘겨줘야 하니까 뒤지게 귀찮은게 아닌가 싶음

주입할 Bean을 좀 더 명확하게 하고 싶다면? @Primary, @Qualifier

🎯 동일 타입 빈 충돌 해결: @Primary & @Qualifier

1. 사용 배경

  • 인터페이스 하나에 여러 구현체가 빈으로 등록된 경우 발생함.
  • 의존성 주입 시 어떤 구현체를 선택할지 스프링에 명시적인 기준을 제공하기 위함임.

2. 주요 전략

  • @Primary: 해당 빈을 기본 우선순위로 설정함. 주입 받는 곳에서 별도 설정이 없으면 이 빈이 채택됨.
  • @Qualifier: 빈에 별칭을 부여하고 주입 시 해당 이름을 명시함. 가장 강력하고 구체적인 선택 방법임.

3. 우선순위 규칙

  • 두 어노테이션이 충돌할 경우, 더 구체적인 @Qualifier가 @Primary보다 우선권을 가짐.
구분@Primary@Qualifier
성격기본값 설정 (우선순위)특정 빈 지목 (정밀 타격)
장점코드가 간결함 (주입 시 추가 어노테이션 불필요)의도가 명확함 (어떤 빈이 주입되는지 바로 알 수 있음)
우선순위낮음높음 (@Qualifier가 이김)

구현체에다가 이거 붙여주면 된다!

4. 요약

다형성을 활용한 설계에서 발생하는 주입 모호성을 해결하는 도구이며, 유지보수 편의를 위해 @Primary를 기본으로 하되 특수한 상황에 @Qualifier를 병용함.

🏗️ 생성자 주입을 권장하는 이유 (DI)

1. 불변성 확보

  • final 키워드 사용이 가능하여 의존관계가 실행 중 변하지 않음을 보장함.
  • 객체의 안정성이 높아지며 조작의 위험을 방지함.

2. 테스트 용이성

  • 스프링 컨테이너 없이도 순수 자바 코드로 의존성을 주입하여 단위 테스트 가능함.
  • Mock 객체 주입이 간편해짐.

3. 완전한 객체 상태

  • 객체 생성 시점에 모든 의존성이 주입되어야 하므로 NPE 발생을 사전에 차단함.
  • 컴파일 시점에 의존성 누락을 확인할 수 있음.

4. 순환 참조 탐지

  • 애플리케이션 구동 시점에 순환 참조 문제를 즉시 발견하여 Fail-fast가 가능함.

5. 요약

생성자 주입은 객체의 안전성테스트 효율성을 극대화하며, 롬복(@RequiredArgsConstructor)과 결합 시 코드 가독성까지 챙길 수 있는 표준 방식임.

profile
제가 공부한 내용을 적은 것이기 때문에 틀릴 수 있습니다.

0개의 댓글