6장. 스프링 컴포넌트 스캔 완벽 정리: 왜 자동 빈 등록이 필요한가?

스프링을 어느 정도 배우고 나면 반드시 마주치는 개념이 있다.

바로 컴포넌트 스캔(Component Scan)이다.

처음 스프링을 배울 때는 @Configuration + @Bean으로 객체를 등록하는 방식으로 시작한다.

하지만 애플리케이션 규모가 조금만 커지면 이 방식은 한계를 드러낸다.

  • 클래스가 20개만 넘어가도 설정 파일이 너무 길어진다
  • 새로운 서비스를 추가할 때마다 AppConfig를 수정해야 한다
  • 개발자가 직접 모든 빈을 등록해야 하므로 실수 가능성이 커진다
  • 유지보수 비용이 기하급수적으로 증가한다

이 문제를 해결하기 위한 스프링의 해법이 바로 컴포넌트 스캔이다.

스프링이 직접 클래스를 찾아 자동으로 빈을 등록하는 방식이다.

이 글에서는 컴포넌트 스캔의 동작 방식, 장점, 주의사항,

그리고 실무에서 반드시 알아야 할 활용법까지 완전하게 정리해본다.


1. 컴포넌트 스캔이 등장한 이유: 빈 등록의 자동화

스프링 초기 버전에서는 모든 빈을 직접 등록해야 했다.

예를 들어 AppConfig가 이렇게 생긴다고 해보자.

@Bean
public MemberService memberService() {
    return new MemberServiceImpl(memberRepository());
}

@Bean
public MemberRepository memberRepository() {
    return new MemoryMemberRepository();
}

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

서비스가 늘어날수록 설정 파일은 폭발적으로 커지게 된다.

A 서비스 → 메서드 2개

B 서비스 → 메서드 3개

C 서비스 → 메서드 5개

이렇게 늘어나기 시작하면 AppConfig는 금방 300~400줄을 넘어간다.

이 문제를 해결하기 위해 스프링은

“스프링이 직접 클래스를 찾아 빈으로 등록해줄게”

라는 철학을 도입했다.

그것이 바로 **컴포넌트 스캔(Component Scan)**이다.


2. 컴포넌트 스캔 사용 방법

컴포넌트 스캔을 활성화하는 방법은 매우 간단하다.

@Configuration
@ComponentScan
public class AutoAppConfig {

}

이 두 줄이면 스프링 컨테이너는 다음을 자동으로 수행한다.

  1. classpath를 스캔한다
  2. @Component가 붙은 클래스를 모두 찾는다
  3. 해당 클래스의 객체를 생성한다
  4. 스프링 빈으로 등록한다
  5. 필요한 의존관계를 자동으로 연결한다(@Autowired )

즉,“스프링이 알아서 AppConfig를 대신 작성해주는 것” 이라고 보면 된다.


3. @Component가 붙은 클래스는 모두 자동 빈 등록

컴포넌트 스캔을 활성화하면

스프링은 @Component가 붙은 클래스만 수집해 빈으로 등록한다.

스프링에서 자주 사용하는 대표적인 컴포넌트들이 있다.

  • @Component
  • @Service
  • @Repository
  • @Controller
  • @RestController
  • @Configuration

이 모든 어노테이션은 결국 내부에 @Component가 포함되어 있다.

그러므로 다음 코드는 모두 컴포넌트 스캔 대상이다.

@Service
public class MemberServiceImpl implements MemberService {}

@Repository
public class MemoryMemberRepository implements MemberRepository {}

@Component
public class RateDiscountPolicy implements DiscountPolicy {}

@Controller
public class MemberController {}

스프링은 각 파일을 스캔하면서 클래스 역할에 맞는 빈을 자동으로 등록한다.

개발자는 더 이상 AppConfig에서 일일이 @Bean 메서드를 작성할 필요가 없다.


4. 의존관계 자동 주입: @Autowired

컴포넌트 스캔을 사용할 때 가장 강력한 기능이 바로 @Autowired이다.

예:

@Service
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository,
                            DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}

스프링은 다음 과정을 자동으로 수행한다.

  1. OrderServiceImpl를 스캔해 빈으로 등록
  2. 생성자를 확인
  3. 파라미터 타입에 맞는 빈들을 찾음
  4. 자동으로 주입

이 과정 전체가 개발자 개입 없이 이루어진다.

여기서 우리는 강력한 장점을 얻는다.

  • DIP 준수
  • 의존성 교체가 쉬움
  • 테스트 편의성 증가
  • 설정 파일 최소화

5. 스프링 부트가 빠른 이유: 자동 컴포넌트 스캔

스프링 부트가 “설정 없이 실행된다”라고 하는 이유가 바로 컴포넌트 스캔 때문이다.

스프링 부트 프로젝트를 생성하면 기본 구조는 이렇게 시작한다.

@SpringBootApplication
public class MySpringApp {

    public static void main(String[] args) {
        SpringApplication.run(MySpringApp.class, args);
    }
}

여기서 @SpringBootApplication 안에는

@ComponentScan이 이미 포함되어 있다.

즉, 스프링 부트는 자동으로 아래 패키지를 스캔한다.

  • MySpringApp 클래스가 있는 위치
  • 그 하위 패키지 전체

그렇기 때문에 개발자는 단지 클래스를 작성하고 @Component, @Service, @Repository만 붙이면

바로 사용이 가능하다.

AppConfig를 작성할 필요조차 없다.


6. 컴포넌트 스캔의 기본 탐색 위치

스프링은 클래스가 있는 패키지를 기준으로 하위 경로를 모두 스캔한다.

예를 들어 스프링 부트 프로젝트에서 기본 패키지 구조는 이렇게 생겼다.

com.example.myapp
  ├─ controller
  ├─ service
  ├─ repository
  └─ domain

스프링은 com.example.myapp 패키지를 기준으로

하위 모든 패키지를 스캔한다.

그래서 스프링 공식 권장 패키지 구조가 나온다.

스프링 부트 메인(Application 클래스)은 프로젝트 루트 패키지에 위치시켜라.

그 이유가 바로 컴포넌트 스캔 때문이다.


7. 스캔 범위를 커스터마이징하는 방법

때때로 스캔 범위를 조절해야 하는 경우가 있다.

예를 들어 특정 패키지만 스캔하고 싶다면:

@ComponentScan(basePackages = "com.example.service")

여러 패키지도 가능하다.

@ComponentScan(basePackages = {
    "com.example.service",
    "com.example.repository"
})

필터링 기능도 사용할 수 있다.

포함 필터

@Component 외 특정 어노테이션만 스캔하고 싶을 때

제외 필터

특정 클래스를 스캔 대상에서 제외하고 싶을 때

이런 필터 기능은 대규모 프로젝트에서 유용하게 쓰인다.


8. 자동 등록 vs 수동 등록 충돌 시 어떤 것이 우선될까?

컴포넌트 스캔과 수동 등록(@Bean)이 충돌하는 상황이 생길 수 있다.

예:

  • @Component로 자동 등록된 DiscountPolicy
  • @Bean으로 등록된 DiscountPolicy

스프링 규칙은 다음과 같다.

  1. 기본적으로 수동 등록이 자동 등록을 덮어쓴다
  2. 하지만 최근 스프링은 충돌 시 오류를 발생시키도록 개선되었다

이는 “명시적인 설정은 최대한 안전하게 처리하겠다”라는 철학 때문이다.


9. 실무에서 컴포넌트 스캔을 사용할 때 주의해야 할 점

컴포넌트 스캔은 정말 강력하지만 주의해야 하는 포인트들도 있다.

1) 의존성 두 개 이상일 때 에러 발생

DiscountPolicy가 Fix, Rate 두 개가 있을 경우

@Autowired
private DiscountPolicy discountPolicy;

이렇게 하면 어떤 정책을 넣어야 할지 스프링이 모른다.

해결책:

  • @Primary
  • @Qualifier
  • Map<String, DiscountPolicy> 주입
  • @Configuration의 수동 등록

2) 순환 참조(circular dependency) 주의

A → B → C → A 형태는 치명적인 문제를 일으킨다.

3) 의존성 찾기 어려움

자동 등록이기 때문에 구조가 복잡하면 어디서 빈이 주입되는지 찾기 어려울 수 있다.


결론: 컴포넌트 스캔은 스프링 개발의 표준 방식이다

스프링을 조금만 규모 있게 사용하려면

수동 빈 등록 방식(AppConfig)은 금방 한계에 부딪힌다.

반면 컴포넌트 스캔은 다음 장점을 제공한다.

  • 설정 파일이 거의 필요 없다
  • 클래스에 @Component 계열 어노테이션만 붙이면 사용 가능
  • 패키지 구조만 잘 설계하면 자동으로 빈들이 구성된다
  • 개발 속도가 비약적으로 향상된다
  • 유지보수성이 크게 좋아진다
  • 스프링 부트와의 호환성이 뛰어나다

이제 스프링 개발의 표준은

“@ComponentScan + @Autowired” 조합이다.

DI(의존관계 자동 주입), AOP, 스프링 빈 생명주기, 스프링 부트 자동 설정 등

스프링의 대부분의 기능이 컴포넌트 스캔 기반 위에서 동작한다.

따라서 컴포넌트 스캔의 흐름을 정확히 이해하면

스프링 애플리케이션 구조 전체가 한눈에 보이기 시작한다.