Spring의 핵심은 두 단어로 요약됩니다: IoC(Inversion of Control)와 DI(Dependency Injection). "내가 객체를 만드는 게 아니라 Spring이 만들어서 나한테 준다" — 이 역전이 Spring 전체의 기반입니다. 이 챕터는 IoC 컨테이너가 어떻게 동작하고, DI 방식 3가지 중 왜 생성자 주입이 권장되는지를 다룹니다.
전통적인 코드에서는 내가 필요한 객체를 직접 new로 생성합니다.
IoC는 이 제어권을 프레임워크(Spring 컨테이너)에 넘기는 것입니다.
객체 생성, 의존관계 설정, 소멸 — 전부 Spring이 관리합니다.
// 개발자가 직접 new로 생성
// 의존 관계가 코드에 하드코딩
public class OrderService {
private UserRepository userRepository;
private PaymentClient paymentClient;
public OrderService() {
// 직접 생성 → 강한 결합
this.userRepository = new UserRepositoryImpl();
this.paymentClient = new PaymentClientImpl();
// 테스트 시 mock 교체 불가능
// 구현체가 바뀌면 이 코드도 바뀜
}
}
// Spring이 필요한 Bean을 만들어서 주입
// 인터페이스에만 의존 → 느슨한 결합
@Service
public class OrderService {
private final UserRepository userRepository;
private final PaymentClient paymentClient;
// 생성자 주입 — Spring이 호출
public OrderService(
UserRepository userRepository,
PaymentClient paymentClient) {
this.userRepository = userRepository;
this.paymentClient = paymentClient;
// 테스트 시 mock 주입 가능
}
}
BeanFactory는 Bean 등록·조회 기능만 있는 최소 컨테이너입니다.
ApplicationContext는 BeanFactory를 상속하고
국제화(MessageSource), 이벤트 발행(ApplicationEventPublisher), 환경 변수(Environment), AOP 통합까지 추가합니다.
Spring Boot는 항상 AnnotationConfigServletWebServerApplicationContext 같은 ApplicationContext 구현체를 씁니다.
실무에서 BeanFactory를 직접 쓸 일은 없습니다.
@Component가 붙은 클래스는 Spring이 스캔해서 Bean으로 등록합니다.
기술적으로는 모두 @Component의 특수화(specialization)입니다 — 기능 차이는 없지만
역할을 명시하고 일부는 추가 기능을 제공합니다.
| 애노테이션 | 대상 계층 | 추가 기능 |
|---|---|---|
@Component |
범용 (어느 계층이든) | 없음 — 기본 Bean 등록 |
@Service |
비즈니스 로직 계층 | 없음 (의미론적 구분만) |
@Repository |
데이터 접근 계층 | DB 예외를 Spring DataAccessException으로 자동 변환 |
@Controller |
MVC 웹 계층 | DispatcherServlet이 요청 매핑 대상으로 인식 |
@RestController |
REST API 계층 | @Controller + @ResponseBody 합성 — 모든 메서드에 @ResponseBody 적용 |
@Configuration |
설정 클래스 | CGLIB 프록시로 감싸 @Bean 메서드가 항상 같은 인스턴스를 반환하도록 보장 (Singleton 보장) |
@Service
public class OrderService {
@Autowired
private UserRepository userRepository;
@Autowired
private PaymentClient paymentClient;
// 문제점:
// - final 선언 불가 → 불변성 없음
// - 테스트 시 mock 주입 어려움
// (Spring 없이 new로 생성 시
// userRepository = null)
// - 의존관계가 숨겨짐
}
@Service
public class OrderService {
private NotificationClient notif;
@Autowired(required = false)
public void setNotif(
NotificationClient notif) {
this.notif = notif;
}
// 용도: 선택적 의존성
// (없어도 동작하는 경우)
// 문제: 주입 안 된 채 사용 가능
// → NPE 위험
}
@Service
@RequiredArgsConstructor // Lombok
public class OrderService {
private final UserRepository repo;
private final PaymentClient pay;
// 장점:
// - final → 불변성 보장
// - 생성 시 의존성 완전 주입
// (NPE 불가능)
// - 테스트: new OrderService(mock)
// - 순환 의존성 컴파일 시 감지
// @RequiredArgsConstructor =
// final 필드 생성자 자동 생성
}
@Autowired는 주입 메커니즘입니다 — Bean을 찾아서 넣어주는 도구일 뿐,
Singleton을 만드는 것이 아닙니다.
Singleton은 Bean 등록 방식에 의해 결정됩니다.
@Component, @Service, @Repository로 등록된 Bean은 기본적으로 Singleton Scope입니다.
ApplicationContext 전체에서 해당 타입의 인스턴스가 딱 하나 생성되고, @Autowired는 그 하나의 인스턴스를 찾아서 주입합니다.
같은 인터페이스를 구현한 Bean이 2개 이상이면 Spring은 어느 것을 주입해야 할지 알 수 없어
NoUniqueBeanDefinitionException을 던집니다.
public interface PaymentClient {
void pay(long amount);
}
@Component
@Primary // 특별히 지정하지 않으면 이게 선택됨
public class NaverPayClient implements PaymentClient { ... }
@Component
public class KakaoPayClient implements PaymentClient { ... }
@Service
public class OrderService {
private final PaymentClient client;
// → NaverPayClient 자동 선택
public OrderService(PaymentClient client) {
this.client = client;
}
}
@Component("naverPay")
public class NaverPayClient implements PaymentClient { ... }
@Component("kakaoPay")
public class KakaoPayClient implements PaymentClient { ... }
@Service
public class OrderService {
private final PaymentClient client;
public OrderService(
@Qualifier("kakaoPay") // 명시적으로 지정
PaymentClient client) {
this.client = client;
}
}
// @Primary보다 @Qualifier가 우선함
// 커스텀 qualifier annotation도 만들 수 있음
IoC는 객체의 생성과 의존관계 관리 제어권을 개발자가 아닌 프레임워크에 넘기는 설계 원칙입니다. DI는 IoC를 구현하는 구체적인 기법 중 하나로, 필요한 객체를 외부(Spring 컨테이너)에서 주입해주는 방식입니다. IoC는 개념, DI는 그 개념을 실현하는 방법이라고 볼 수 있습니다.
세 가지 이유가 있습니다.
첫째, final 키워드로 불변성을 보장할 수 있습니다 — 생성 후 의존성이 바뀔 수 없습니다.
둘째, Spring 없이 new OrderService(mockRepo, mockPay)로 단위 테스트가 가능합니다.
필드 주입은 Spring 컨테이너 없이 주입할 방법이 없어 테스트가 어렵습니다.
셋째, 생성자 주입은 순환 의존성(A→B→A)을 애플리케이션 시작 시 즉시 감지해 예외를 던집니다.
필드/세터 주입은 런타임에서야 StackOverflow로 발견됩니다.
기능적으로 가장 큰 차이는 예외 변환입니다.
@Repository가 붙은 클래스는 Spring이 AOP 프록시를 적용해
DB 드라이버나 JPA에서 던지는 예외(SQLException, JPA PersistenceException)를
Spring의 DataAccessException 계층 구조로 자동 변환합니다.
이 덕분에 서비스 계층이 특정 DB 기술에 종속된 예외를 처리하지 않아도 됩니다.
Spring IoC는 "내가 객체를 만드는 게 아니라 Spring이 만들어서 주입해준다"는 제어 역전이며, DI 방식 중 생성자 주입이 불변성·테스트 용이성·순환 의존성 조기 탐지 세 가지를 모두 충족해서 권장된다.