1. Spring IoC/DI 기초

Spring의 핵심은 두 단어로 요약됩니다: IoC(Inversion of Control)와 DI(Dependency Injection). "내가 객체를 만드는 게 아니라 Spring이 만들어서 나한테 준다" — 이 역전이 Spring 전체의 기반입니다. 이 챕터는 IoC 컨테이너가 어떻게 동작하고, DI 방식 3가지 중 왜 생성자 주입이 권장되는지를 다룹니다.

1. IoC — 제어의 역전

전통적인 코드에서는 내가 필요한 객체를 직접 new로 생성합니다. IoC는 이 제어권을 프레임워크(Spring 컨테이너)에 넘기는 것입니다. 객체 생성, 의존관계 설정, 소멸 — 전부 Spring이 관리합니다.

IoC 없음 — 직접 제어
// 개발자가 직접 new로 생성
// 의존 관계가 코드에 하드코딩
public class OrderService {
    private UserRepository userRepository;
    private PaymentClient paymentClient;

    public OrderService() {
        // 직접 생성 → 강한 결합
        this.userRepository = new UserRepositoryImpl();
        this.paymentClient = new PaymentClientImpl();
        // 테스트 시 mock 교체 불가능
        // 구현체가 바뀌면 이 코드도 바뀜
    }
}
IoC — Spring이 제어
// 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 주입 가능
    }
}

ApplicationContext — Spring IoC 컨테이너

BeanFactory vs ApplicationContext

BeanFactory는 Bean 등록·조회 기능만 있는 최소 컨테이너입니다. ApplicationContextBeanFactory를 상속하고 국제화(MessageSource), 이벤트 발행(ApplicationEventPublisher), 환경 변수(Environment), AOP 통합까지 추가합니다. Spring Boot는 항상 AnnotationConfigServletWebServerApplicationContext 같은 ApplicationContext 구현체를 씁니다. 실무에서 BeanFactory를 직접 쓸 일은 없습니다.

@ComponentScan
클래스패스 탐색
Bean 인스턴스 생성
기본: Singleton
의존관계 주입
DI 수행
ApplicationContext 준비 완료
@PostConstruct 실행

2. @Component 계열 애노테이션

@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 보장)

3. DI 3가지 방식 — 생성자 주입이 권장되는 이유

① 필드 주입
비권장
@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와 Singleton의 관계 — 혼동 주의

@Autowired주입 메커니즘입니다 — Bean을 찾아서 넣어주는 도구일 뿐, Singleton을 만드는 것이 아닙니다.
Singleton은 Bean 등록 방식에 의해 결정됩니다. @Component, @Service, @Repository로 등록된 Bean은 기본적으로 Singleton Scope입니다. ApplicationContext 전체에서 해당 타입의 인스턴스가 딱 하나 생성되고, @Autowired는 그 하나의 인스턴스를 찾아서 주입합니다.

4. 같은 타입 Bean이 여러 개일 때 — @Qualifier / @Primary

같은 인터페이스를 구현한 Bean이 2개 이상이면 Spring은 어느 것을 주입해야 할지 알 수 없어 NoUniqueBeanDefinitionException을 던집니다.

@Primary — 기본 Bean 지정
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;
    }
}
@Qualifier — 명시적 선택
@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도 만들 수 있음

5. 면접 대비 핵심 Q&A

Q1. "IoC와 DI의 차이를 설명해주세요."

IoC는 객체의 생성과 의존관계 관리 제어권을 개발자가 아닌 프레임워크에 넘기는 설계 원칙입니다. DI는 IoC를 구현하는 구체적인 기법 중 하나로, 필요한 객체를 외부(Spring 컨테이너)에서 주입해주는 방식입니다. IoC는 개념, DI는 그 개념을 실현하는 방법이라고 볼 수 있습니다.

Q2. "필드 주입 대신 생성자 주입을 써야 하는 이유는?"

세 가지 이유가 있습니다. 첫째, final 키워드로 불변성을 보장할 수 있습니다 — 생성 후 의존성이 바뀔 수 없습니다. 둘째, Spring 없이 new OrderService(mockRepo, mockPay)로 단위 테스트가 가능합니다. 필드 주입은 Spring 컨테이너 없이 주입할 방법이 없어 테스트가 어렵습니다. 셋째, 생성자 주입은 순환 의존성(A→B→A)을 애플리케이션 시작 시 즉시 감지해 예외를 던집니다. 필드/세터 주입은 런타임에서야 StackOverflow로 발견됩니다.

Q3. "@Repository가 @Component와 다른 점은?"

기능적으로 가장 큰 차이는 예외 변환입니다. @Repository가 붙은 클래스는 Spring이 AOP 프록시를 적용해 DB 드라이버나 JPA에서 던지는 예외(SQLException, JPA PersistenceException)를 Spring의 DataAccessException 계층 구조로 자동 변환합니다. 이 덕분에 서비스 계층이 특정 DB 기술에 종속된 예외를 처리하지 않아도 됩니다.

이 챕터의 핵심 한 줄

Spring IoC는 "내가 객체를 만드는 게 아니라 Spring이 만들어서 주입해준다"는 제어 역전이며, DI 방식 중 생성자 주입이 불변성·테스트 용이성·순환 의존성 조기 탐지 세 가지를 모두 충족해서 권장된다.