ABOUT

성능과 운영 안정성을 함께 끌어올리는 개발자입니다.

92% Positional Error Reduction
79% p95 Latency Improvement
90%+ Long Tasks Reduction

2022.02 · 한국장학재단

우수 멘티

한국장학재단 사회 리더 대학생 멘토링 IT

2022.10 · 동작구청

우수 인재상

동작구청 우수 SW 인재

2025.05 · (주) 그랩

프로그래밍 우수상

(주) 그랩 우수 프로그램 개발

2025.05 · AWSKRUG

AWS한국사용자모임 발표

AI agent 스크립트 튜닝 관련 발표

ComputerScience

Development

Engineering

Trouble Shooting

GUESTBOOK

첫 마음부터
함께 나누는 온기

방명록 작성하러 가기

SUBSCRIBE

최신소식을
편하게 만나보세요.

@Component

도입

“이 클래스를 스프링 컨테이너가 관리하는 Bean으로 등록해도 된다”는 신호입니다.

단순히 붙이면 끝나는 어노테이션처럼 보이지만, 실제로는 컴포넌트 스캔(Component Scanning), BeanDefinition 생성, 생명주기, 스코프, AOP 프록시, 레이어링(Controller/Service/Repository) 같은 핵심 메커니즘과 맞물려 동작합니다.

정의

“스프링이 관리할 객체 후보”를 표시하는 기본 스테레오타입이다

스프링에서 “관리한다”는 말은 곧 스프링 컨테이너(ApplicationContext)가 객체(Bean)의 생성/초기화/의존성 주입/소멸까지 책임진다는 뜻입니다.

@Component는 그 관리 대상이 되기 위한 가장 기본적인 표식이며, 클래스패스 스캐닝 과정에서 발견되면 스프링은 이 클래스를 BeanDefinition(설계도)으로 등록하고 필요 시 실제 객체를 생성합니다.

핵심 요약
  • @Component가 붙은 클래스는 컴포넌트 스캔 대상이 된다.
  • 스캔 결과로 BeanDefinition이 등록되고, 컨테이너가 싱글톤(기본)으로 인스턴스를 관리한다.
  • @Service/@Repository/@Controller는 내부적으로 @Component의 특수화다(= 결국 컴포넌트 스캔 기반).
TIP
“@Component = Bean 등록”이라고만 외우면 실무에서 막힙니다. 실제로는 어떤 패키지를 스캔하는지, Bean 이름이 어떻게 결정되는지, 충돌 시 어떤 예외가 나는지까지 이해해야 디버깅이 빨라집니다.

필요성

@Component는 IoC/DI를 “자동 등록”으로 연결해주는 관문이다

스프링이 강력한 이유는 개발자가 객체 생성(new)과 생명주기 관리에서 벗어나 “구성(설정)과 사용(비즈니스)”을 분리할 수 있기 때문입니다. 그 분리의 실행 장치가 IoC(Inversion of Control), 그리고 의존성을 외부에서 주입하는 DI(Dependency Injection)입니다.

“자동 등록”이 중요한 이유
  • Bean 등록을 일일이 @Bean으로 나열하면 설정이 비대해진다.
  • 클래스 수가 늘어날수록 수동 등록 누락이 빈번해진다.
  • 레이어별(Controller/Service/Repository) 구조에서 자동 스캔이 생산성을 크게 올린다.
import org.springframework.stereotype.Component;

@Component
public class Clock {
  public long now() {
    return System.currentTimeMillis();
  }
}

이렇게만 해도(그리고 스캔 범위에 포함되어 있다면) 스프링은 Clock을 Bean으로 등록하고, 다른 Bean에서 생성자 주입으로 사용할 수 있게 됩니다.

동작 원리

컴포넌트 스캔 → 후보 판별 → BeanDefinition 등록 → 인스턴스 생성/주입
Step-by-step 흐름
  1. 스캔 범위 결정: 어느 패키지를 대상으로 클래스패스를 훑을지 정한다.
  2. 후보 탐색: .class 메타데이터를 읽어 어노테이션/메타어노테이션 기반으로 후보를 찾는다.
  3. 후보 판별: 인터페이스/추상클래스 여부, 필터 규칙 포함/제외 여부 등 조건을 통과해야 한다.
  4. BeanDefinition 등록: “이 타입은 어떤 스코프/이름/생성 방식으로 만들 것인가”를 설계도로 저장한다.
  5. 인스턴스 생성 및 DI: 싱글톤이면 컨테이너 초기화 시점에(보통) 생성하고 의존성을 주입한다.
  6. 후처리: BeanPostProcessor가 개입해 AOP 프록시 생성, @Autowired 처리, @PostConstruct 호출 등이 수행된다.
스캔 범위는 어디서 정해지나?

스프링 부트에서는 보통 @SpringBootApplication이 붙은 메인 클래스의 패키지를 기준으로 “하위 패키지”를 기본 스캔합니다. 즉, 메인 클래스가 com.example에 있으면 기본적으로 com.example.* 아래가 스캔 대상입니다.

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication // 내부적으로 @ComponentScan 포함
public class App { }
실무 함정 1
@Component를 붙였는데 Bean이 안 잡힌다 → 대부분 스캔 범위 문제입니다. 메인 클래스 패키지 “위”에 클래스를 두면 기본 스캔에서 빠집니다.
포함/제외 필터(스캔 제어)

스캔을 “전부” 해도 되지만, 모듈이 커질수록 불필요한 Bean이 잡히거나 테스트 환경에서 원치 않는 Bean이 올라오는 문제가 생깁니다. 이때 includeFilters / excludeFilters로 스캔 대상을 정교하게 제어할 수 있습니다.

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(
  basePackages = "com.example",
  excludeFilters = @ComponentScan.Filter(
    type = FilterType.REGEX,
    pattern = "com\\.example\\.legacy\\..*"
  )
)
public class ScanConfig { }

이름 규칙

Bean 이름은 “기본 규칙 + 충돌 처리”를 이해해야 한다
기본 Bean 이름
  • 기본적으로 클래스의 단순 이름(Simple Name)을 사용하고, 첫 글자를 소문자로 만든다. (예: UserServiceuserService)
  • 단, 첫 두 글자가 모두 대문자인 경우(예: URLParser)는 관례상 소문자화가 적용되지 않는 케이스가 있다.
이름을 명시적으로 지정
import org.springframework.stereotype.Component;

@Component("systemClock")
public class Clock { }

이름을 직접 정하면 Bean 충돌을 피하거나, 레거시/외부 연동에서 특정 이름을 요구할 때 유리합니다. 하지만 무분별한 네이밍은 오히려 유지보수를 어렵게 만들 수 있으니 충돌 해결이 목적일 때만 전략적으로 쓰는 편이 좋습니다.

실무 함정 2
Bean 이름 충돌은 환경/빌드에 따라 다르게 드러날 수 있습니다. 특히 멀티모듈에서 같은 클래스명이 여러 곳에 존재하면 “등록 순서” 차이로 문제를 늦게 발견하기도 합니다.

스코프/생명주기

@Component Bean은 기본이 Singleton이며, 필요 시 Scope/Proxy를 설계해야 한다
기본 스코프: Singleton

스프링의 기본 스코프는 singleton입니다. 즉, 애플리케이션 컨텍스트당 인스턴스가 1개만 생성되어 공유됩니다. 그래서 @Component Bean은 기본적으로 “상태를 들고 있지 않는(stateless)” 설계를 권장합니다.

Prototype / Request 스코프가 필요한 경우
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Component
@Scope("prototype")
public class TraceId {
  private final String value = java.util.UUID.randomUUID().toString();
  public String value() { return value; }
}

prototype은 “요청할 때마다 새 인스턴스”를 만들지만, 컨테이너가 소멸(destroy)까지 관리하지는 않는다는 점이 중요합니다. 즉, 프로토타입 Bean에 자원 정리 로직이 있다면 직접 정리 전략을 잡아야 합니다.

TIP
요청 스코프(request/session) Bean을 싱글톤에 주입해야 한다면, 보통 스코프 프록시(proxyMode) 설계를 함께 고려합니다.
생명주기 훅: 초기화/소멸
  • @PostConstruct: 의존성 주입 완료 이후 초기화
  • @PreDestroy: 컨텍스트 종료 시 소멸 콜백(싱글톤 중심)
  • 또는 InitializingBean/DisposableBean, @Bean(initMethod/destroyMethod) 등 다양한 방식이 있다

비교

@Component / @Bean / @Service·@Repository·@Controller의 차이를 “의도”로 구분하라
방식 등록 단위 강점 추천 사용처
@Component 클래스 자동 스캔/등록, 레이어 구조에 자연스러움 애플리케이션 내부 구성요소(도메인 서비스, 유틸, 어댑터 등)
@Bean 메서드 외부 라이브러리/생성 로직 제어/조건부 조립에 강함 서드파티 객체 등록, 생성 파라미터/팩토리/빌더가 필요한 경우
@Service 클래스 서비스 레이어 의도 명확(팀 합의/툴링 가독성) 유스케이스/비즈니스 로직 중심
@Repository 클래스 일부 환경에서 예외 변환(퍼시스턴스 예외 처리)에 도움 DB 접근/영속성 레이어
@Controller 클래스 웹 핸들러로 인식(MVC, 라우팅) 웹 요청 처리(REST는 @RestController)
핵심 결론
기술적으로는 대부분 “스캔 등록”이라는 결과가 같지만, 실무에서는 의도를 드러내는 어노테이션을 선택하는 것이 유지보수에 유리합니다. 예: 비즈니스 로직은 @Service, DB 접근은 @Repository, 웹 엔드포인트는 @Controller/@RestController.

실전 패턴

생성자 주입 + 다형성 충돌 해결(@Qualifier/@Primary)을 묶어서 이해하라
권장: 생성자 주입
import org.springframework.stereotype.Component;

@Component
public class OrderFacade {
  private final PaymentClient paymentClient;

  public OrderFacade(PaymentClient paymentClient) {
    this.paymentClient = paymentClient;
  }
}

생성자 주입은 필수 의존성 강제, 불변성, 테스트 용이성에서 장점이 큽니다.

다형성 충돌: 같은 타입 Bean이 여러 개면?

예를 들어 PaymentClient 구현체가 2개(@Component)라면, 스프링은 “어떤 Bean을 넣어야 할지” 결정 못하고 예외를 던집니다. 이때 @Qualifier 또는 @Primary가 해법이 됩니다.

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

public interface PaymentClient { }

@Component
@Primary
class CardPaymentClient implements PaymentClient { }

@Component("bankPay")
class BankPaymentClient implements PaymentClient { }
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
public class CheckoutService {
  private final PaymentClient paymentClient;

  public CheckoutService(@Qualifier("bankPay") PaymentClient paymentClient) {
    this.paymentClient = paymentClient;
  }
}

AOP/프록시

@Component는 AOP의 “대상”이 될 뿐, 프록시 생성은 후처리 단계에서 일어난다

@Transactional, @Async, @Cacheable 같은 기능은 대개 AOP 프록시를 통해 동작합니다. 중요 포인트는 “@Component가 프록시를 만든다”가 아니라, Bean으로 등록된 후 BeanPostProcessor가 필요에 따라 프록시로 감싼다는 것입니다.

자주 겪는 함정: 자기 호출(self-invocation)
같은 클래스 내부에서 this.someMethod()로 호출하면 프록시를 거치지 않아 @Transactional 등이 기대대로 동작하지 않을 수 있습니다. (해결: 메서드 분리/구조 분리, 외부 Bean을 통한 호출, 설계 재조정 등)

GOOD / BAD

@Component를 “아무 데나” 붙이는 습관은 구조를 흐린다
GOOD ✅
  • 레이어 의도가 명확하면 @Service/@Repository/@Controller 사용
  • 도메인/어댑터/유틸 성격이면 @Component로 단순화
  • 스캔 범위는 “필요한 패키지”로 좁히고 모듈 경계를 지킨다
BAD ❌
  • 모든 클래스에 무지성 @Component → 레이어링/역할이 흐려짐
  • 전 패키지 무차별 스캔 → 테스트/운영에서 원치 않는 Bean이 올라옴
  • 상태를 가진 싱글톤 Bean 남발 → 동시성 버그/테스트 난이도 증가

고급

@Component는 메타 어노테이션 기반으로 확장할 수 있다

스프링의 스테레오타입은 “@Component가 붙은 어노테이션”을 통해 확장 가능합니다. 즉, 우리 팀/도메인에 맞춘 “역할 어노테이션”을 만들어 코드 의도를 더 선명하게 표현할 수 있습니다.

import org.springframework.stereotype.Component;
import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface DomainAdapter {
  String value() default "";
}
@DomainAdapter
public class KakaoPayAdapter { }
TIP
커스텀 스테레오타입은 “표준화된 패키지 구조 + 역할 명명 규칙”과 함께 쓸 때 가장 효과가 큽니다.

디버깅

@Component가 동작하지 않을 때: 체크 순서를 고정해두면 해결이 빨라진다
1
패키지 스캔 범위를 확인한다. (메인 클래스 패키지 기준 하위 패키지인가?)
2
클래스가 추상/인터페이스가 아닌지, 내부 클래스/익명 클래스로 이상하게 구성되지 않았는지 확인한다.
3
excludeFilters나 프로파일/조건(@Profile, @Conditional 등)로 인해 제외된 것은 아닌지 확인한다.
4
동일한 타입/이름 Bean이 여러 개 등록되어 충돌 난 것은 아닌지 확인한다. (예: NoUniqueBeanDefinitionException)
5
“등록은 됐는데 기능이 안 돈다”면 AOP 프록시/자기호출/빈 초기화 순서를 의심한다.

요약

실무에서 @Component를 제대로 쓰기 위한 체크리스트
  • ✅ 스캔 범위(패키지 경계)를 설명할 수 있는가?
  • ✅ Bean 이름 규칙/충돌 해결(@Qualifier/@Primary)을 알고 있는가?
  • ✅ 싱글톤 기본 전제를 지키며 상태를 최소화했는가?
  • ✅ 레이어 의도에 맞춰 @Service/@Repository/@Controller를 선택했는가?
  • ✅ “등록은 됐는데 동작이 이상함”에서 AOP/프록시/자기호출을 의심할 수 있는가?
  • ✅ 외부 라이브러리는 @Bean으로, 내부 구성요소는 @Component로 구분했는가?
728x90