C
Spring Boot/핵심개념/Lesson 04

IoC · DI · Bean — Spring 의 *심장*

45분·theory

IoC · DI · Bean — Spring 의 *심장*

🎯 이 lesson 을 읽고 나면

이 lesson 을 다 읽고 나면 아래 3가지를 자신 있게 할 수 있습니다.

  • ✅ @Autowired 가 어떻게 Bean 을 주입하는지 (3가지 방법)
  • ✅ 같은 타입 Bean 충돌 시 @Qualifier / @Primary 해결
  • ✅ @Configuration + @Bean 으로 외부 라이브러리 등록

학습 목표를 체크리스트로 두고 다 답할 수 있게 되면 lesson 을 닫으세요.

IoC (제어의 역전) — *내가 만들지 않고 받아쓴다*

핵심 한 줄

IoC (Inversion of Control) = 객체를 내가 직접 만드는 게 아니라 프레임워크가 만들어서 내게 건네주는 방식. 제어의 흐름이 뒤집혔다 는 뜻.

옛 방식 — 모든 걸 직접 만들기

java
public class OrderService {
    private UserRepository userRepo = new UserRepository();     // 직접 생성
    private EmailService email = new EmailService();             // 직접 생성

    public void order(Long userId) {
        User u = userRepo.findById(userId);
        email.send(u.email(), "주문 완료");
    }
}

겉보기엔 깔끔하지만 큰 문제 가 있습니다:

  • 테스트 어려움: OrderService 만 테스트하고 싶은데 진짜 EmailService 가 같이 동작 — 메일이 실제 발송됨
  • 수정 어려움: EmailServiceSmsService 로 바뀌면 모든 호출 코드 수정
  • 의존성 추적 어려움: 어디서 누가 누구를 만드는지 추적 불가

새 방식 — 받아쓰기

java
@Service
public class OrderService {
    private final UserRepository userRepo;
    private final EmailService email;

    public OrderService(UserRepository userRepo, EmailService email) {
        this.userRepo = userRepo;        // Spring 이 넣어줌
        this.email = email;
    }

    public void order(Long userId) {
        User u = userRepo.findById(userId);
        email.send(u.email(), "주문 완료");
    }
}

이제 OrderService어디서 만들어졌는지 신경 안 씁니다. 그냥 받아서 씁니다. 누가 만들지는 Spring 의 책임.

이게 IoC. 객체 생성의 제어권을 프레임워크에 넘긴 것.

왜 좋은가

1. 테스트가 쉽다:

java
@Test
void 주문_테스트() {
    EmailService fakeEmail = new MockEmailService();   // 가짜
    OrderService svc = new OrderService(userRepo, fakeEmail);
    svc.order(42L);
    // fakeEmail 호출 여부 검증 — 실제 메일 안 감
}

2. 구현 교체가 쉽다: EmailServiceKakaoMessageService 로 바꾸려면 Spring 설정 한 줄 만. 호출 코드는 그대로.

3. 의존성이 명확하다: 생성자 시그니처만 봐도 무엇이 필요한지 한눈에.

IoC 컨테이너 — Spring 의 공장

객체들을 만들고 보관하고 연결해주는 Spring 의 핵심 부품을 IoC 컨테이너 또는 ApplicationContext 라 합니다.

동작 흐름:
1. 앱 시작 시 Spring 이 @Component·@Service·@Repository·@Controller 가 붙은 클래스를 모두 스캔
2. 발견한 각 클래스의 인스턴스를 1개씩 만들어 컨테이너에 보관
3. 다른 곳에서 그 객체가 필요하면 생성자·@Autowired주입

> 💡 컨테이너에 보관된 객체를 Bean 이라고 부릅니다. 콩 (bean) 처럼 작은 단위 객체 모음이라는 비유.

한 번 정리

  • IoC = 객체 생성을 프레임워크에 위임
  • 장점 = 테스트·교체·추적이 모두 쉬워짐
  • Spring 컨테이너 = Bean 들을 만들고 연결하는 공장

이게 Spring 의 가장 근본 사상. DI·AOP·트랜잭션 모두 IoC 위에서 동작합니다.

DI (의존성 주입) — *세 가지 방법*

DI 와 IoC 의 관계

IoC 가 더 큰 개념 이고, DI (Dependency Injection) 는 그 실현 수단 중 하나입니다. 객체에게 필요한 의존성을 외부에서 주입 하는 구체적 기법.

3가지 주입 방법

1. 생성자 주입 (가장 권장)

java
@Service
public class OrderService {
    private final UserRepository userRepo;

    public OrderService(UserRepository userRepo) {
        this.userRepo = userRepo;
    }
}

장점:

  • final 가능 → 불변 보장
  • 객체 생성 시점에 모든 의존성 보장
  • 순환 참조 시 컴파일 타임 에 발견
  • 테스트 시 가짜 객체 주입 쉬움

Spring 4.3+ 부터 @Autowired 생략 가능. Lombok @RequiredArgsConstructor 와 결합하면 더 짧음:

java
@Service
@RequiredArgsConstructor
public class OrderService {
    private final UserRepository userRepo;
    private final EmailService email;
    // 생성자 자동 생성
}

2. Setter 주입

java
@Service
public class OrderService {
    private UserRepository userRepo;

    @Autowired
    public void setUserRepo(UserRepository userRepo) {
        this.userRepo = userRepo;
    }
}

장점: 선택적 의존성 (있을 수도 없을 수도) 표현 가능
단점: final 못 씀. 호출 안 되면 null. 거의 안 씀.

3. 필드 주입 (지양)

java
@Service
public class OrderService {
    @Autowired private UserRepository userRepo;       // ❌
}

겉보기엔 간결하지만:

  • 테스트 어려움 (Reflection 으로만 주입 가능)
  • 순환 참조 런타임 까지 모름
  • 의존성이 너무 쉬워서 무분별하게 추가

> 💡 현장 합의: 항상 생성자 주입. 필드 주입은 옛 코드에만 보임.

같은 타입이 여러 개일 때

java
@Service public class EmailService implements MessageService { }
@Service public class SmsService   implements MessageService { }

@Service
public class OrderService {
    public OrderService(MessageService msg) { }   // ❌ 모호함
}

Spring 이 어느 걸 주입 해야 할지 모릅니다. 3가지 해결:

1. @Primary — 기본값 지정:

java
@Service
@Primary
public class EmailService implements MessageService { }

2. @Qualifier — 명시적 선택:

java
public OrderService(@Qualifier("emailService") MessageService msg) { }

3. List 로 받기 — 모든 구현 받기:

java
public OrderService(List<MessageService> all) {
    // 모든 MessageService 구현체
}

흔한 함정

순환 참조: A 가 B 를 주입받고, B 가 A 를 주입받는 경우. 생성자 주입에선 시작 시 에러. 설계가 잘못된 신호 — 공통 부분을 새 클래스로 분리 가 정답.

@Autowired vs @Resource vs @Inject: Spring 표준 @Autowired 만 알아도 무방. 다른 둘은 Java 표준 인데, 실무에선 거의 안 쓰임.

한 번 정리

  • 생성자 주입이 표준. Lombok @RequiredArgsConstructor 와 결합 추천
  • 같은 타입 여러 개면 @Primary 또는 @Qualifier
  • 순환 참조는 설계 신호 — 리팩토링 필요

Bean — *Spring 이 관리하는 객체*

Bean 이 뭐냐

Spring IoC 컨테이너에 등록되어 관리되는 객체Bean 입니다. 처럼 작은 단위 객체 모음이라는 비유. 일반 자바 객체와 다른 점은 Spring 이 생성·라이프사이클·주입을 모두 관리 한다는 것.

Bean 등록 방법

1. @Component 계열 (가장 흔함)

java
@Component        // 일반
@Service          // 비즈니스 로직
@Repository       // DB 접근
@Controller       // 웹 컨트롤러
@RestController   // REST API

이름은 다르지만 기본적으로 Bean 등록 이라는 점은 같습니다. 의미적 구분 + 일부 기능 차이 (@Repository 는 예외 변환).

java
@Service
public class UserService {
    // 자동으로 Bean 으로 등록됨
}

2. @Bean (메서드 단위)

설정 클래스의 메서드 리턴 값을 Bean 으로:

java
@Configuration
public class AppConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }
}

내 코드가 아닌 외부 라이브러리 클래스 (RestTemplate·ObjectMapper 등) 를 Bean 으로 만들 때 사용.

3. 자동 설정 — Spring Boot Starter

spring-boot-starter-data-jpa 같은 starter 의존성이 자동으로 수많은 Bean 등록 (DataSource·EntityManager·TransactionManager 등).

Bean Scope — 언제 새로 만드나

기본은 singleton — 컨테이너에 하나만 만들고 모두 공유. 하지만 다른 옵션도 있습니다.

Scope의미용도
singleton (기본)컨테이너당 1개거의 모든 경우
prototype요청마다 새 객체상태 보유하는 객체
requestHTTP 요청당 1개요청 단위 상태
sessionHTTP 세션당 1개세션 단위 상태
applicationServletContext당 1개글로벌
java
@Service
@Scope("prototype")
public class StatefulProcessor { }

가장 흔한 함정: singleton Bean 에 변경되는 필드 두기. 멀티스레드 환경에서 데이터 깨짐. 항상 불변 또는 외부 저장소 (DB·Redis) 사용.

Bean 라이프사이클

Bean 은 다음 단계를 거칩니다:

1. 인스턴스 생성 (생성자 호출)
2. 의존성 주입 (@Autowired 등)
3. 초기화 (@PostConstruct 또는 InitializingBean.afterPropertiesSet())
4. 사용
5. 소멸 (@PreDestroy 또는 DisposableBean.destroy())

java
@Service
public class CacheManager {
    @PostConstruct
    public void init() {
        // Bean 생성 후 1번 실행. 캐시 로딩 등
    }

    @PreDestroy
    public void cleanup() {
        // 앱 종료 시 1번 실행. 자원 정리
    }
}

@PostConstruct 는 자주 씁니다 — 자원 준비·캐시 워밍 등. @PreDestroyGraceful Shutdown 에 중요.

@Configuration vs @Component

둘 다 Bean 으로 등록되지만 역할이 다름.

  • @Configuration — 설정 클래스. 안의 @Bean 메서드를 프록시로 감싸 동일 객체 반환 보장
  • @Component — 일반 Bean

@Configuration 안에서 @Bean 메서드를 서로 호출 하면 같은 인스턴스 가 리턴됩니다. @Component 면 매번 새 객체가 생기는 버그 가능성.

한 번 정리

  • Bean = Spring 이 관리하는 객체
  • 등록: @Service 등 어노테이션 또는 @Bean 메서드
  • 기본 Scope 는 singleton (대부분 OK)
  • @PostConstruct·@PreDestroy 로 라이프사이클 훅 가능

@Bean · @Configuration · @Qualifier · @Primary 충돌 해결

@Component vs @Bean — 언제 뭘 쓰나

  • @Component (@Service·@Repository·@Controller 포함) — 내가 만든 클래스 에. Spring 이 자동 스캔.
  • @Bean외부 라이브러리 객체 또는 조건부 등록 이 필요할 때.

@Configuration + @Bean 예시

java
@Configuration
public class AppConfig {

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplateBuilder()
            .setConnectTimeout(Duration.ofSeconds(3))
            .setReadTimeout(Duration.ofSeconds(5))
            .build();
    }

    @Bean
    public ObjectMapper objectMapper() {
        return new ObjectMapper()
            .registerModule(new JavaTimeModule())
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    }
}

외부 라이브러리 클래스 (RestTemplate·ObjectMapper) 는 내가 @Component 를 붙일 수 없으니 @Bean 으로 등록.

같은 타입 Bean 이 2개 — 충돌

java
@Bean PaymentService cardPayment() { ... }
@Bean PaymentService kakaoPayment() { ... }

@Autowired
PaymentService paymentService;   // ❌ NoUniqueBeanDefinitionException

같은 타입이 여러 개 → Spring 이 어떤 걸 주입해야 할지 모름.

해결 1 — @Qualifier 로 이름 지정

java
@Autowired
@Qualifier("kakaoPayment")
PaymentService paymentService;

Bean 이름은 메서드명 이 기본 (cardPayment·kakaoPayment).

해결 2 — @Primary 로 기본 선택

java
@Bean @Primary
PaymentService cardPayment() { ... }   // 기본값

@Bean
PaymentService kakaoPayment() { ... }

@Autowired PaymentService p;   // ✅ cardPayment 주입 (Primary)

둘 중 뭘 쓰나

  • 기본/주력 구현이 명확@Primary
  • 상황별로 다른 걸 주입@Qualifier
  • 둘 다 안 쓰고 이름으로 자동 매칭 도 가능 (@Autowired PaymentService cardPayment;)

조건부 등록 — @ConditionalOnProperty

java
@Bean
@ConditionalOnProperty(name = "payment.provider", havingValue = "kakao")
public PaymentService kakaoPayment() { ... }

application.yml 의 payment.provider=kakao 일 때만 Bean 등록. 프로파일별 다른 구현 을 깔끔하게 분리할 수 있어요.

🤖 AI 에게 이렇게 요청해보세요

이 lesson 의 개념을 알면 AI 에게 구체적으로 지시할 수 있습니다. 막연한 "고쳐줘" 가 아니라 어휘를 가진 요청 — 그게 토큰 절약의 출발점입니다.

  • "이 PaymentService 의 구현체가 2개인데 @Qualifier 로 분기해줘"
  • "이 RestTemplate 를 @Configuration + @Bean 으로 등록해줘"
  • "@ConditionalOnProperty 로 카카오 결제 모듈을 조건부 활성화해줘"

왜 이게 토큰을 줄이나

개념을 모를 땐 AI 답변을 받고도 "그게 뭐예요?" 를 다시 물어야 합니다. 그 "다시 물음" 이 토큰을 잡아먹습니다. 개념 한 번 익혀두면 대화가 한 번에 끝납니다.

IoC · DI · Bean — Spring 의 심장 - Spring Boot