이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • @Component 계열 애너테이션과 @Bean은 언제 다르게 선택해야 하는가?
  • Component Scan은 어떤 범위를 스캔하고 어떤 클래스를 Bean으로 등록하는가?
  • Bean 이름과 등록 위치가 왜 설계 정보가 되는가?

개요

Spring은 Bean을 여러 방식으로 등록할 수 있다. 대표적인 방식은 컴포넌트 스캔과 Java 기반 설정이다. 컴포넌트 스캔은 @Component와 그 메타 애너테이션이 붙은 클래스를 찾아 BeanDefinition으로 등록한다. Java 설정은 @Configuration 클래스의 @Bean 메서드를 통해 객체 생성 방식을 명시적으로 등록한다.

두 방식은 경쟁 관계가 아니다. 애플리케이션의 서비스, 컨트롤러, 리포지토리처럼 직접 작성한 계층 객체는 컴포넌트 스캔이 자연스럽고, 외부 라이브러리 객체나 생성 과정이 복잡한 객체는 @Bean이 자연스럽다.

Component Scan

컴포넌트 스캔은 특정 base package 아래에서 stereotype 애너테이션이 붙은 클래스를 찾는다. Spring Boot의 @SpringBootApplication은 선언된 패키지를 기준으로 컴포넌트 스캔을 시작하므로, 애플리케이션 클래스의 위치도 스캔 범위에 영향을 준다.

@Component
public class EmailSender {
}
 
@Service
public class OrderService {
}
 
@Repository
public class OrderRepository {
}

@Service, @Repository, @Controller는 모두 @Component 기반이다. 등록 기능만 보면 비슷하지만 계층 의미가 다르다. 이 의미는 코드 읽기, persistence 예외 변환, MVC 매핑, AOP Pointcut 설계에 영향을 준다.

Java 설정과 @Bean

@Bean은 메서드가 반환한 객체를 Bean으로 등록한다.

@Configuration
public class ClientConfig {
 
    @Bean
    public PaymentClient paymentClient() {
        return new PaymentClient("https://api.example.com");
    }
}

이 방식은 외부 라이브러리 클래스처럼 소스에 @Component를 붙일 수 없는 객체를 등록할 때 유용하다. 생성 과정에서 설정 값, builder, factory method가 필요한 경우에도 @Bean이 읽기 쉽다.

Bean 이름

Bean 이름은 보통 클래스명이나 메서드명에서 파생된다. OrderService 클래스는 기본적으로 orderService라는 이름을 갖고, paymentClient() 메서드는 paymentClient라는 이름의 Bean을 만든다. 다만 커스텀 stereotype 이름 규칙이나 name generator를 쓰면 기본 규칙이 달라질 수 있다.

Bean 이름은 평소에는 숨겨져 있지만 여러 상황에서 중요해진다.

  • 같은 타입 후보가 여러 개일 때 이름이 선택 기준이 될 수 있다.
  • AOP Pointcut의 bean() 지정자에서 이름이 매칭 기준이 된다.
  • 테스트에서 특정 Bean을 대체할 때 이름이 충돌 지점이 된다.
  • 운영 설정에서 조건부 Bean을 분리할 때 이름과 역할이 문서 역할을 한다.

등록 방식 선택 기준

등록 방식을 고를 때는 다음 질문을 던진다.

  • 내가 작성한 애플리케이션 계층 객체인가?
  • 외부 라이브러리 객체인가?
  • 생성 과정이 단순한가, 설정 값과 builder가 필요한가?
  • 계층 의미를 코드에 드러내야 하는가?
  • 같은 타입의 여러 Bean이 필요한가?

서비스, 리포지토리, 컨트롤러는 컴포넌트 스캔으로 등록하면 계층 의미가 자연스럽다. 외부 client, mapper, factory, 라이브러리 adapter는 @Bean으로 등록하면 생성 과정을 한곳에 모을 수 있다.

이 기준은 절대 규칙이 아니라 기본값이다. 애플리케이션 클래스라도 생성 순서와 설정 값이 중요하면 @Bean이 더 읽기 쉬울 수 있고, 외부 라이브러리를 감싼 adapter를 직접 작성했다면 adapter 자체는 @Component로 등록해도 자연스럽다.

실전 팁

  • @Component를 모든 클래스에 붙이지 않는다. DTO, Entity, 값 객체는 보통 Bean이 아니다.
  • @Service@Repository는 단순 취향이 아니라 계층 의도다. 나중에 AOP Pointcut, persistence 예외 변환, 계층별 테스트 범위를 정할 때 도움이 된다.
  • 외부 라이브러리 객체는 설정 클래스에서 @Bean으로 감싸면 테스트 대체와 설정 주입이 쉬워진다.
  • Bean 이름은 API는 아니지만 운영 지식이다. 같은 타입 Bean을 여러 개 만들 때 이름을 의도적으로 짓는다.
  • 스캔 범위가 너무 넓으면 테스트용 컴포넌트, 실험용 설정, 의도하지 않은 @Configuration까지 끌려올 수 있다.

위험 신호!

  • base package를 넓게 잡아 의도하지 않은 Bean이 등록된다.
  • 유틸 클래스, DTO, Entity까지 전부 @Component로 만든다.
  • 외부 라이브러리 객체 생성 코드가 여러 서비스에 흩어져 있다.
  • 같은 타입 Bean이 여러 개인데 이름이나 qualifier 전략이 없다.
  • @Service@Repository를 의미 없이 섞어 쓴다.
  • 애플리케이션 시작 클래스 위치가 바뀌었는데 컴포넌트 스캔 범위 변화를 확인하지 않는다.

확인 질문

확인 질문

  • 컴포넌트 스캔은 무엇을 찾는가?
    • 스캔 범위 안에서 @Component와 그 계열 애너테이션이 붙은 클래스를 찾아 BeanDefinition으로 등록한다.
  • @Bean이 특히 유용한 경우는 언제인가?
    • 외부 라이브러리 객체나 생성 과정이 복잡한 객체를 명시적으로 등록할 때 유용하다.
  • Bean 이름이 중요한 순간은 언제인가?
    • 같은 타입 후보가 여러 개이거나 AOP bean() Pointcut, 테스트 대체, 조건부 설정이 얽힐 때 중요하다.

참고 문서