← 개인 공부 · Java
STUDY NOTE · 개념 정리

스프링 IoC와 DI 개념 정리: 빈, 주입 방식, 스코프

Java
IoC(제어의 역전)는 객체의 생성과 연결을 개발자가 아니라 프레임워크(컨테이너)가 맡는 것이고, DI(의존성 주입)는 그 IoC를 구현하는 방법으로, 객체가 필요한 의존성을 외부에서 넣어주는 것이다.

1. 의존성(Dependency)이란

A가 동작하려면 B가 필요할 때 "A는 B에 의존한다"고 한다. OrderService가 결제를 위해 PaymentClient를 쓴다면 OrderService는 PaymentClient에 의존한다. 문제는 누가 B를 만들고 고르느냐다.

// A가 직접 B를 생성: 구현체 선택까지 A가 책임짐
public class OrderService {
private final PaymentClient client = new TossPaymentClient();
}

// 외부에서 B를 받음: A는 역할(인터페이스)만 알면 됨
public class OrderService {
private final PaymentClient client;
public OrderService(PaymentClient client) { this.client = client; }
}

2. 직접 생성 vs 주입

구분직접 생성 (new)의존성 주입 (DI)
구현체 선택사용하는 클래스가 결정외부(컨테이너, 설정)가 결정
결합도높음 (구현체에 묶임)낮음 (인터페이스에만 의존)
구현 교체사용하는 쪽 코드 수정 필요설정만 변경
테스트가짜 객체로 바꾸기 어려움생성자로 가짜 객체 전달 가능
객체 수명 관리개발자가 직접컨테이너가 관리

DI는 SOLID의 DIP(의존 역전 원칙), "구체가 아니라 추상에 의존하라"를 실천하는 수단이기도 하다.

3. IoC 컨테이너와 빈

  1. IoC 컨테이너: 스프링에서는 ApplicationContext. 객체를 만들고, 의존성을 연결하고, 생명주기를 관리한다.
  2. 빈(Bean): 컨테이너가 생성하고 관리하는 객체.
등록 방법방식언제 쓰나
컴포넌트 스캔클래스에 @Component, @Service, @Repository, @Controller직접 만든 클래스
자바 설정@Configuration 클래스의 @Bean 메서드외부 라이브러리 객체, 생성 로직이 복잡할 때

@Service, @Repository, @Controller는 모두 @Component의 특수화로, 역할을 드러내는 이름표다. @Repository는 DB 예외를 스프링 예외로 변환하는 기능이 추가된다.

4. 빈 생명주기

  1. 컨테이너 생성
  2. 빈 인스턴스 생성
  3. 의존성 주입
  4. 초기화 콜백 (@PostConstruct)
  5. 사용
  6. 소멸 전 콜백 (@PreDestroy)
  7. 컨테이너 종료

의존성은 3단계에서 들어오므로, 생성자 안에서는 생성자 주입으로 받은 것만 쓸 수 있고, 필드·수정자 주입 값은 @PostConstruct 이후에 써야 한다.

5. 주입 방식 비교

구분생성자 주입수정자(Setter) 주입필드 주입
형태생성자 파라미터setXxx 메서드 + @Autowired필드에 @Autowired
final 사용가능 (불변 보장)불가불가
누락 시객체 생성 자체가 실패 (빠른 발견)null로 남을 수 있음null로 남을 수 있음
테스트new로 쉽게 생성가능스프링 없이 넣을 방법이 없음
순환 참조기동 시점에 감지늦게 발견늦게 발견
권장기본선택적 의존성테스트 코드 외에는 지양

생성자가 하나면 @Autowired를 생략할 수 있고(Spring 4.3+), 롬복 @RequiredArgsConstructor와 함께 쓰면 final 필드만 선언하면 된다.

6. 같은 타입의 빈이 여러 개일 때

방법설명
@Primary여러 후보 중 기본으로 쓸 빈 지정
@Qualifier("이름")주입받는 쪽에서 특정 빈을 이름으로 선택
List / Map으로 받기해당 타입의 빈 전부를 주입 (Map은 빈 이름이 key)

아무것도 지정하지 않으면 NoUniqueBeanDefinitionException이 발생한다.

7. 빈 스코프

스코프생성 단위비고
singleton컨테이너당 1개기본값
prototype요청(주입)할 때마다 새로생성 후에는 컨테이너가 관리하지 않음 (소멸 콜백 없음)
requestHTTP 요청당 1개웹 환경
sessionHTTP 세션당 1개웹 환경

싱글톤 빈은 여러 요청(스레드)이 동시에 같은 객체를 공유한다. 그래서 싱글톤 빈에 요청별로 바뀌는 값을 필드로 저장하면 다른 사용자의 값과 섞인다. 싱글톤 빈은 무상태(stateless)로 설계해야 한다.

8. 실제로 어떻게 적용되나

  1. 결제 수단별 전략 패턴: PaymentClient 인터페이스의 구현(Toss, 카드사 등)을 List<PaymentClient>로 전부 주입받아 Map으로 만들어 두면, 결제 수단이 추가돼도 구현 클래스 하나만 추가하면 된다. "같은 타입의 빈 여러 개를 컬렉션으로 주입"을 활용한 것이다.
  2. 다중 DataSource: DB가 두 개면 같은 타입(DataSource)의 빈이 두 개가 된다. 하나에 @Primary를 붙이고 다른 쪽은 @Qualifier로 골라 쓴다.
  3. 테스트: 생성자 주입이면 new OrderService(fakePaymentClient)처럼 스프링 없이 가짜 객체로 유닛 테스트할 수 있다.
  4. @Transactional과 프록시: 컨테이너는 원본 빈 대신 프록시를 주입할 수 있다. 트랜잭션, 캐시 같은 기능이 이 방식으로 붙는다. 그래서 같은 클래스 안에서 자기 메서드를 직접 호출하면 프록시를 거치지 않아 @Transactional이 적용되지 않는다.

9. 순환 참조

A가 B를, B가 A를 생성자로 받으면 어느 쪽도 먼저 만들 수 없다. Spring Boot 2.6부터는 순환 참조를 기본적으로 금지해 기동 시점에 실패한다. 주입 방식을 바꿔 우회하기보다 공통 로직을 별도 클래스로 분리해 구조를 고치는 게 원칙이다.

10. 면접 질문으로 정리

  1. Q. IoC와 DI의 차이는? IoC는 객체 생성과 의존 관계 관리의 제어권을 개발자에서 컨테이너로 넘기는 원칙이고, DI는 필요한 의존성을 외부에서 주입하는 방식으로 IoC를 구현하는 방법이다.
  2. Q. 생성자 주입을 권장하는 이유는? final로 불변성을 보장하고, 의존성이 누락되면 객체 생성 시점에 바로 실패하며, 스프링 없이도 테스트에서 직접 생성할 수 있고, 순환 참조를 기동 시점에 발견할 수 있기 때문이다.
  3. Q. 스프링 빈은 기본적으로 싱글톤인데 주의할 점은? 여러 스레드가 같은 인스턴스를 공유하므로 요청마다 바뀌는 상태를 필드에 두면 안 된다. 무상태로 설계해야 한다.
  4. Q. 같은 타입 빈이 여러 개면? @Primary로 기본 빈을 지정하거나, @Qualifier로 이름을 지정하거나, List·Map으로 전부 주입받는다.
Gunmo Lee
스프링 IoC와 DI 개념 정리: 빈, 주입 방식, 스코프 | Gunmo's Dev Life