◆ TURNING · SPRING

DI — 의존성 주입

@Autowired 의 마법

학습 목표

  • DI (Dependency Injection) 의 의미를 안다
  • 3 가지 주입 방식을 안다 (필드·생성자·setter)
  • @Autowired 가 자동으로 무엇을 하는지 안다
  • 등록 어노테이션 4 가지 (@Component·@Service·@Repository·@Controller)

⚠️ IoC 만으로는 막연

"개념은 알겠는데 — 코드는?"

전 차시 IoC 는 「발상」 까지. 「Spring 으로는 어떻게 코드를 짜는가?」 가 다음 단계.

전 차시 실습에서는 main 에서 직접 주입했지만 — Spring 은 이를 자동화합니다.

🛠️ DI — 의존성 주입

의존 객체를 외부에서 끼워넣는 방식

「Dependency Injection」 = 클래스가 필요한 부품을 외부에서 받아쓰는 패턴. IoC 의 가장 일반적인 구현.

비유 — 재료 배달 요리사가 직접 장 보러 가지 않음 → 필요한 재료가 자동으로 주방으로 배달 → 요리사는 요리에만 집중

Spring 코드 — 핵심 두 줄


@Service                         // ⭐ ① Bean 등록
public class OrderService {

    @Autowired                   // ⭐ ② 자동 주입
    private MessageSender sender;

    public void order() {
        sender.send("주문 완료");
    }
}

👉 두 어노테이션이 「IoC + DI」 의 전부. 컨테이너가 알아서 처리.

등록 어노테이션 4 가지

어노테이션의미적 자리
@Component일반 Bean (특별한 의미 없음)
@Service비즈니스 로직 계층
@Repository데이터 접근 계층
@Controller웹 요청 진입점

👉 기능적으로는 모두 동일 — Spring 이 같은 일을 함. 의미적 표시일 뿐.

주입 ① 필드 주입


@Service
class OrderService {
    @Autowired
    private MessageSender sender;
}

👉 가장 간단한 방식. 학습 단계와 짧은 예제에 적합합니다.

다만 final 을 못 붙이고, 테스트 시 객체에 의존성을 끼워 넣기 어렵습니다.

주입 ② 생성자 주입 (권장)


@Service
class OrderService {
    private final MessageSender sender;

    @Autowired      // 생성자가 1 개면 생략 가능
    public OrderService(MessageSender sender) {
        this.sender = sender;
    }
}

👉 final 가능 / 테스트 쉬움 / 순환 참조를 컴파일 시 검출. 실무 표준.

주입 ③ setter 주입


@Service
class OrderService {
    private MessageSender sender;

    @Autowired
    public void setSender(MessageSender sender) {
        this.sender = sender;
    }
}

👉 객체 생성 후에 의존성을 바꿀 수 있는 「가변」 주입. 잘 쓰지 않습니다 — 객체가 「설정 누락」 상태로 잠시 존재할 수 있기 때문.

주입 방식 비교

방식장점단점
필드코드 짧음final 불가, 테스트 어려움
생성자 ⭐final, 테스트 쉬움, 순환 참조 검출생성자 길어질 수 있음
setter가변 의존성객체 생성 후 누락 가능

👉 실무 표준 = 생성자 주입. 학습용 = 필드 주입 OK.

v1 → v2 — 수동 vs Spring

전 차시 (수동 IoC)

// main 에서
MessageSender s
  = new GmailSender();
OrderService svc
  = new OrderService(s);
svc.order();
이번 차시 (Spring DI)

@Service
class OrderService {
  @Autowired
  MessageSender sender;
}
// 끝!

@Autowired 의 동작

Tomcat 시작 ↓ Spring 컨테이너 시작 ↓ component-scan 으로 모든 @Component·@Service·... 클래스 발견 ↓ 각 클래스의 인스턴스 자동 생성 (Bean 등록) ↓ 각 Bean 의 @Autowired 필드를 찾음 ↓ 필드 타입에 맞는 Bean 을 컨테이너에서 검색 ↓ 필드에 자동 주입 ↓ 모든 Bean 의 의존성 그래프 완성 → 애플리케이션 가동

실수 — Bean 미등록


// @Service 빠뜨림
public class OrderService {
    @Autowired
    private MessageSender sender;
}

// 실행 시
NoSuchBeanDefinitionException: No qualifying bean of type 'OrderService'

👉 등록 어노테이션이 없으면 Bean 으로 안 만들어짐 → 다른 곳에서 주입 불가.

실수 — 같은 타입 여러 Bean


@Service public class GmailSender implements MessageSender { ... }
@Service public class KakaoSender implements MessageSender { ... }

@Service
public class OrderService {
    @Autowired
    private MessageSender sender;   // 둘 중 어느 것?
}
증상

"NoUniqueBeanDefinitionException: expected single matching bean but found 2"

해결: @Primary 또는 @Qualifier("gmailSender") 로 지정.

@Qualifier — 특정 Bean 선택


@Service
@Primary                   // 기본 Bean 으로 지정
public class GmailSender implements MessageSender { ... }

@Service("kakao")
public class KakaoSender implements MessageSender { ... }

// 주입 시 명시적으로
@Autowired
@Qualifier("kakao")
private MessageSender sender;

실험 — 부품 교체

  1. GmailSender, KakaoSender 두 클래스 작성
  2. 한 쪽에만 @Service 붙임 → 그 Bean 이 주입됨
  3. @Service 를 다른 쪽으로 옮김 → OrderService 코드 한 줄도 안 바뀐 채 동작 변경
  4. 「부품 교체에 OrderService 가 흔들리지 않음」 직접 체험

🔄 Before / After

전 차시 끝

IoC 의 발상은 안다. 직접 main 에서 주입.

이번 차시 끝

@Autowired 한 줄로 자동 주입. 등록 어노테이션 4 가지를 안다.

📊 한 그림 정리

이번 차시의 데이터 흐름

Spring 컨테이너
@Autowired
OrderService 의 필드
「@Autowired」 가 컨테이너와 클래스 사이의 자동 다리

정리

오늘 들고 가는 것

  • DI = 의존성을 외부에서 주입
  • @Service / @Component / @Repository / @Controller 로 등록
  • @Autowired 로 자동 주입
  • 주입 3 가지: 필드 / 생성자(권장) / setter
  • 같은 타입 여러 개면 @Primary 또는 @Qualifier