공부하는 안씨의 기록

[정리] 백엔드 기초 - 의존성 주입 (DI) 본문

KT AIVLE 8기 AI 트랙 활동

[정리] 백엔드 기초 - 의존성 주입 (DI)

an씨 2025. 11. 25. 09:12

DI - 의존성 주입

필요한 객체(의존성)를 외부에서 넣어주는 것

  • 방법: 생성자 주입 / 세터 주입 / 필드 주입(@ Autowired)

(1) 생성자 주입

  • final 사용 가능 → 불변성 보장
  • 의존성 누락 방지(주입 안되면 컴파일 에러)
  • 테스트 용이
  • 순환 참조 문제 조기 발견
@Component
public class Jihyun {
    private final Phone phone;

    @Autowired
    public Jihyun(Phone phone) {
        this.phone = phone;
    }
}

 

(2) 필드 주입

  • 의존성이 보이지 않고 숨겨져 있음
  • 생성자 호출 시 완전한 객체가 아니게 됨
  • 순환 참조 문제 늦게 발견됨
  • spring 없으면 이 코드 자체가 작동 불가→ 강한 스프링 종속
@Component
public class Jihyun {
    @Autowired
    private Phone phone; 
    **// 바로 필드에 꽂아버림**
}

 

생성자 주입 예시 코드(Phone에 Galaxy 만 implement 하는 경우)

  • Jihyun은 Phone 인터페이스만 알고 있음 → 결합도↓, 테스트↑, 교체 용이
import org.springframework.stereotype.Component;

@Component
public class Galaxy implements Phone {
    @Override
    public void powerOn() {
        System.out.println("Hello Galaxy");
    }
    
    @Override
    public void payment(){
		    System.out.println("Use SamsungPay");
    }
}

@Component
public class Jihyun { **//지현 클래스는 galaxy나 iphone을 직접 생성하지 않음**
    private final Phone phone;

    // 스프링이 Phone 타입 빈을 찾아 자동 주입
    public Jihyun(Phone phone) {  
        this.phone = phone;
    }

    public void usePhone() {
        phone.powerOn();
        phone.payment();
    }
}
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackages = "com.example.demo") // 패키지 경로
public class AppConfig {
}
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
    public static void main(String[] args) {
        ApplicationContext ctx =
                new AnnotationConfigApplicationContext(AppConfig.class);

        Jihyun jihyun = ctx.getBean(Jihyun.class);
        jihyun.usePhone();  // Hello Galaxy
    }
}

❗만일 IPhone 클래스가 추가되면?

@Primary : 기본 폰은 Galxy로 선언하기

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Component
**@Primary   // Phone 주입 시 기본으로 선택될 구현체**
public class Galaxy implements Phone {
    @Override
    public void powerOn() {
        System.out.println("Hello Galaxy");
    }

    @Override
    public void payment() {
        System.out.println("Use SamsungPay");
    }
}

@Component
public class Iphone implements Phone {
    @Override
    public void powerOn() {
        System.out.println("Hello iPhone");
    }

    @Override
    public void payment() {
        System.out.println("Use ApplePay");
    }
}

//////////////////////////////////////////
import org.springframework.stereotype.Component;

@Component
public class Jihyun {   // 여전히 구현체를 모름(Phone 인터페이스만 의존)
    private final Phone phone;

    public Jihyun(Phone phone) {  // Phone 타입 요청
        this.phone = phone;
    }

    public void usePhone() {
        phone.powerOn();
        phone.payment();
    }
}

@Qualifier : Jihyun은 Galaxy만 쓰겠다고 명시

  • 사실 @Component만 써도 bean 이름은 자동으로 galaxy, iphone이라서 @Component("galaxy") 는 생략 가능 (명시적으로 써주면 더 눈에 잘 보임)
  • 지현은 Galaxy, 다른 사람은 Iphone 처럼 사람마다 다른 구현체를 쓰고 싶을 때 서비스/컨트롤러별로 @Qualifier를 다르게 줄 수 있음
import org.springframework.stereotype.Component;

@Component("galaxy")   // bean 이름은 기본이 className camelCase라서 생략 가능
public class Galaxy implements Phone {
    @Override
    public void powerOn() { System.out.println("Hello Galaxy"); }
    @Override
    public void payment() { System.out.println("Use SamsungPay"); }
}

@Component("iphone")
public class Iphone implements Phone {
    @Override
    public void powerOn() { System.out.println("Hello iPhone"); }
    @Override
    public void payment() { System.out.println("Use ApplePay"); }
}
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
public class Jihyun {
    private final Phone phone;

    **public Jihyun(@Qualifier("galaxy") Phone phone) {**
        this.phone = phone;
    }

    public void usePhone() {
        phone.powerOn();
        phone.payment();
    }
}
  • 위 두 방법은 들 다 jihyun은 인터페이스(Phone에만 의존한다는 것.)
  • 이렇게 하면 DI 조건이 깨짐(이렇게는 x. 의존성 주입이 X)
    • 이러면 Jihyun은 Galaxy에 강하게 결합되고,
    • 나중에 Iphone으로 바꿀 때 코드 수정을 직접 해야 함
    • 테스트 시 Mock/Stub 주입도 힘듦
    • -> IoC/DI 원칙 위반