클래스에 애노테이션을 붙이면 컴포넌트 스캔이 찾아서 등록합니다. 역할에 맞는 것을 골라 씁니다.
- @Service: 비즈니스 로직
- @Repository: 데이터 접근
- @Controller / @RestController: 웹 요청 처리
- @Component: 그 외 일반 컴포넌트
Spring Boot 실무 가이드 · Part 3
Spring의 실제 동작을 좌우하는 빈 등록과 주입 방식 이해하기
작성 기준2026년 7월버전과 지원 현황은 이후 달라질 수 있으니 공식 문서를 함께 확인하세요.
이 파트에서 다루는 내용
빈(Bean)은 Spring 컨테이너가 관리하는 객체입니다. 컨테이너가 만들고, 연결하고, 필요할 때 꺼내 씁니다. 등록 방법은 내가 작성한 클래스인지 아닌지로 갈립니다.
클래스에 애노테이션을 붙이면 컴포넌트 스캔이 찾아서 등록합니다. 역할에 맞는 것을 골라 씁니다.
@Configuration 클래스 안의 메서드가 반환하는 객체를 빈으로 등록합니다. 소스를 수정할 수 없는 외부 클래스에 씁니다.
// 내가 만든 클래스: 애노테이션으로 등록
@Service
public class OrderService {
}
// 외부 라이브러리 클래스: @Bean 메서드로 등록
@Configuration
public class HttpConfig {
@Bean
public RestClient restClient() {
return RestClient.builder()
.baseUrl("https://api.example.com")
.build();
}
}@Service 대신 @Component를 써도 동작은 같습니다. 다만 역할이 드러나는 애노테이션을 쓰면 코드를 읽는 사람이 계층을 바로 알 수 있습니다.
주입 방식은 생성자, 필드, setter 세 가지가 있습니다. 결론부터 말하면 생성자 주입을 기본으로 씁니다. 취향 문제가 아니라 이유가 분명합니다.
선택적 의존성이거나 실행 중 교체가 필요한 드문 경우에만 씁니다. 일반적인 서비스 클래스에는 쓰지 않습니다.
// 생성자가 하나면 @Autowired를 생략할 수 있습니다
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentClient paymentClient;
public OrderService(OrderRepository orderRepository,
PaymentClient paymentClient) {
this.orderRepository = orderRepository;
this.paymentClient = paymentClient;
}
}
// Lombok을 쓰면 final 필드 기준으로 생성자를 만들어 줍니다
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentClient paymentClient;
}@RequiredArgsConstructor는 편하지만 팀에서 Lombok 사용 여부를 먼저 합의해야 합니다. 쓰지 않는 팀도 많습니다.
인터페이스 하나에 구현체가 여러 개일 때 Spring은 무엇을 넣을지 판단하지 못하고 기동에 실패합니다. NoUniqueBeanDefinitionException이 이 경우입니다.
여러 구현체 중 하나를 기본으로 정합니다. 대부분의 경우 이 구현을 쓴다는 의미입니다.
주입받는 쪽에서 어떤 빈을 쓸지 이름으로 명시합니다. 상황에 따라 다른 구현을 써야 할 때 적합합니다.
List나 Map으로 받으면 해당 타입 빈이 모두 들어옵니다. 결제 수단별 처리기처럼 여러 구현을 순회할 때 유용합니다.
public interface PaymentClient { }
@Component("cardPayment")
public class CardPaymentClient implements PaymentClient { }
@Component("pointPayment")
public class PointPaymentClient implements PaymentClient { }
// 1) 이름으로 지정
@Service
public class OrderService {
private final PaymentClient paymentClient;
public OrderService(@Qualifier("cardPayment") PaymentClient paymentClient) {
this.paymentClient = paymentClient;
}
}
// 2) 전부 주입받아 상황에 따라 선택
@Service
public class PaymentDispatcher {
private final Map<String, PaymentClient> clients;
public PaymentDispatcher(Map<String, PaymentClient> clients) {
this.clients = clients; // 빈 이름이 key로 들어옵니다
}
}빈은 기본적으로 싱글톤입니다. 애플리케이션 전체에서 하나만 만들어 공유합니다. 그래서 빈에 요청별로 달라지는 상태를 필드로 두면 여러 요청이 같은 값을 건드리게 됩니다.
순환 참조는 A가 B를 주입받고 B가 다시 A를 주입받는 구조입니다. Spring Boot 2.6부터는 기본적으로 허용되지 않고 기동이 실패합니다.
빈의 인스턴스 필드에 요청별 데이터를 저장하면 동시 요청에서 값이 섞입니다. 요청 데이터는 메서드 파라미터와 지역 변수로 다룹니다.
요청할 때마다 새 인스턴스를 만듭니다. 다만 싱글톤 빈이 prototype 빈을 주입받으면 처음 한 번만 생성되므로 의도대로 동작하지 않습니다.
순환 참조 오류를 만나면 우회 설정을 찾기 전에 클래스 책임을 먼저 의심해야 합니다. 대부분 두 서비스가 서로의 기능을 조금씩 필요로 하는 상태이고, 공통 부분을 분리하면 자연스럽게 풀립니다.
본문은 현장에서 가장 많이 쓰는 3.x 기준입니다. 4.x에서 달라진 부분만 아래에 정리합니다.