구성 클래스와 컴포넌트 스캔 예제

예제 목표

이 예제는 @Configuration, @Bean, @ComponentScan을 기준으로 빈 등록 방식과 의존관계 구성을 비교합니다.

먼저 @Configuration 클래스가 프록시로 등록되는 이유를 살펴봅니다. 이어서 @Bean을 사용한 수동 등록과 @ComponentScan을 사용한 방식을 비교하며, 각 방식에서 빈이 어떻게 등록되고 연결되는지 정리합니다.

프로젝트 구조

프로젝트 구조는 아래와 같습니다.

example.configscan
├── config
│   └── ScanConfig
├── domain
│   ├── DiscountPolicy
│   ├── FixedDiscountPolicy
│   ├── MemoryOrderRepository
│   ├── OrderRepository
│   └── OrderService
├── manual
│   └── AppConfig
├── scan
│   ├── ScanDiscountPolicy
│   ├── ScanOrderRepository
│   └── ScanOrderService
├── step1.configuration
├── step2.beanregistration
└── step3.componentscan

@Configuration 클래스

@Configuration 클래스는 기본 설정에서 스프링이 생성한 프록시 클래스로 등록됩니다. 이 프록시는 같은 설정 클래스 안에서 @Bean 메서드를 직접 호출하더라도, 매번 새 객체를 생성하지 않고 컨테이너에 등록된 빈을 반환하도록 동작합니다.

AppConfig appConfigBean = context.getBean(AppConfig.class);

boolean isProxy = appConfigBean.getClass().getName().contains("SpringCGLIB"); // true
boolean sameRepositoryInstance = orderService.getOrderRepository() == orderRepository; // true

AppConfig는 원본 클래스 그대로 등록되지 않고, 스프링이 생성한 프록시 클래스 형태로 등록됩니다.

또한 orderService() 안에서 orderRepository()를 직접 호출하더라도, 최종적으로 같은 OrderRepository 빈 인스턴스가 사용됩니다.

@Configuration 프록시는 @Bean 메서드 호출을 가로채 컨테이너가 관리하는 싱글톤 빈을 반환합니다. 이 때문에 설정 클래스 내부에서 @Bean 메서드를 호출해 의존 객체를 전달하더라도, 이미 등록된 빈 인스턴스가 재사용됩니다.

@Bean으로 수동 등록하기

@Bean 메서드는 반환한 객체를 스프링 빈으로 등록합니다. 메서드 이름은 기본 빈 이름으로 사용됩니다.

@Configuration
public class AppConfig {

    @Bean
    public OrderRepository orderRepository() {
        return new MemoryOrderRepository();
    }

    @Bean
    public DiscountPolicy discountPolicy() {
        return new FixedDiscountPolicy(1_000);
    }

    @Bean
    public OrderService orderService() {
        return new OrderService(orderRepository(), discountPolicy());
    }
}

이 방식에서는 어떤 구현체를 빈으로 등록하는지, 생성자에 어떤 값을 전달하는지 설정 코드에 직접 드러납니다. 예제의 orderRepository, discountPolicy, orderService는 각각 @Bean 메서드 이름을 기본 빈 이름으로 사용합니다.

@Bean을 사용한 수동 등록은 구현체 선택과 의존관계 조립을 설정 코드에서 명시적으로 관리할 때 적합합니다.

@ComponentScan으로 자동 등록하기

@ComponentScan은 지정한 패키지 아래에서 컴포넌트 후보를 찾아 빈으로 등록합니다.

@Configuration
@ComponentScan(basePackages = "example.configscan.scan")
public class ScanConfig {
}

스캔 대상 패키지에는 @Repository, @Component, @Service가 붙은 클래스가 있습니다. @Repository@Service@Component를 기반으로 한 어노테이션이므로, 컴포넌트 스캔 대상에 포함됩니다.

@Repository
public class ScanOrderRepository implements OrderRepository {
}

@Component
public class ScanDiscountPolicy implements DiscountPolicy {
}

@Service
public class ScanOrderService {

    private final OrderRepository orderRepository;
    private final DiscountPolicy discountPolicy;

    public ScanOrderService(OrderRepository orderRepository, DiscountPolicy discountPolicy) {
        this.orderRepository = orderRepository;
        this.discountPolicy = discountPolicy;
    }
}

이 방식에서는 설정 클래스에 @Bean 메서드를 하나씩 작성하지 않아도 됩니다. 스프링은 스캔 대상 패키지에서 컴포넌트 후보를 찾고, 클래스 이름을 기반으로 기본 빈 이름을 생성합니다.

예제의 ScanOrderService, ScanOrderRepository, ScanDiscountPolicy는 각각 scanOrderService, scanOrderRepository, scanDiscountPolicy라는 이름으로 등록됩니다.

ScanOrderService는 생성자를 통해 OrderRepositoryDiscountPolicy를 주입받습니다. 이처럼 컴포넌트 스캔 방식에서는 빈 등록은 어노테이션과 스캔 설정으로 처리되고, 의존관계는 생성자 주입을 통해 구성됩니다.

댓글