Part 2. IoC 컨테이너와 Bean
2.1 IoC와 DI의 개념 (제어의 역전과 의존성 주입)
Spring Framework를 이해할 때 가장 근간이 되는 두 축은 IoC(Inversion of Control, 제어의 역전)와 DI(Dependency Injection, 의존성 주입)입니다. 많은 초심자가 IoC를 단순히 "Spring이 객체를 대신 생성해 주는 편리한 기능" 정도로 오해하지만, 이는 결과물 중 일부일 뿐입니다. IoC와 DI의 본질은 객체의 생명주기 제어권을 프레임워크에 위임하고, 객체 간의 결합도를 낮추기 위한 구조적 설계 메커니즘에 있습니다.
1. 제어의 역전 (IoC)이란?
전통적인 자바 프로그래밍에서는 개발자가 작성한 코드가 객체의 생성부터 생명주기 전체를 직접 제어했습니다.
- 생성 제어: 언제 new 키워드로 객체를 만들 것인가?
- 구현체 결정: 어떤 구체(Concrete) 클래스를 사용할 것인가?
- 연결 및 생명주기 제어: 생성한 객체를 누구에게 전달하고 언제 파괴할 것인가?
// 개발자가 모든 제어권을 직접 쥐고 있는 구조
public class MemberService {
// MemberService가 MemoryMemberRepository 구현체를 직접 선택하고 생성함
private final MemberRepository repository = new MemoryMemberRepository();
}
이 방식은 직관적이지만, MemberService가 저장소 구현체에 강하게 결합(High Coupling)되어 저장 기술(JPA, MyBatis 등)이 바뀔 때마다 서비스 코드를 직접 수정해야 하는 치명적인 단점이 존재합니다. IoC(Inversion of Control)는 이러한 객체의 생성 및 관리 책임을 개발자가 아닌 프레임워크(Spring 컨테이너)로 전적으로 넘기는 설계 원칙입니다.
2. 의존성 주입 (DI)이란?
MemberService가 DB에 접근하기 위해 MemberRepository를 사용하는 것처럼, "한 객체가 작동하기 위해 다른 객체의 기능을 가져다 쓰는 관계"를 의존성(Dependency)이라고 합니다. IoC가 "제어권을 컨테이너에 위임한다"는 상위 차원의 설계 원칙이라면, DI(Dependency Injection)는 이 IoC 원칙을 코드 수준에서 완성하는 구체적인 기술 패턴입니다. 객체가 의존 대상을 직접 생성하지 않고, 외부(Spring 컨테이너)로부터 주입받아 사용합니다.
@Service
public class MemberService {
private final MemberRepository repository;
// 객체를 직접 생성하지 않고, 외부(Spring)에서 생성된 객체를 전달(주입)받음
public MemberService(MemberRepository repository) {
this.repository = repository;
}
public void signUp() {
repository.save(...); // 주입받은 객체의 기능만 '사용'에 집중
}
}
이제 MemberService 내부에는 new 키워드가 존재하지 않습니다. 어떤 구현체가 들어올지 고민할 필요 없이, 오직 본연의 비즈니스 로직을 수행하는 책임만 남게 됩니다.
3. Spring IoC/DI 컨테이너의 동작 구조
Spring은 애플리케이션 시작 시점에 IoC 컨테이너를 통해 객체를 생성하고 의존성을 수동/자동으로 연결합니다.

IoC와 DI의 관계 재정립
- IoC (설계 원칙): "객체 제어의 주도권을 개발자에서 프레임워크로 넘긴다."
- DI (구현 패턴): "IoC를 달성하기 위해, 필요로 하는 의존 객체를 외부에서 주입해 준다."
4. IoC와 DI가 제공하는 핵심 이점
개발자가 직접 `new`를 사용하지 않고 IoC/DI 구조를 취함으로써 얻는 이점은 다음과 같습니다.
- 낮은 결합도 (Loose Coupling): 구체 클래스가 아닌 인터페이스에 의존하므로, 구현체가 바뀌어도 클라이언트 코드는 단 한 줄도 수정할 필요가 없습니다.
- 단일 책임 원칙 (SRP) 준수: 객체는 자신의 핵심 역할(비즈니스 로직)에만 집중하고, 객체의 생성 및 연결은 컨테이너가 전담합니다.
- 테스트 용이성 (Testability): 단위 테스트 작성 시 실제 DB 연동 객체 대신 가짜 객체(Mock/Fake)를 생성자를 통해 손쉽게 주입할 수 있습니다.
// 테스트 코드에서 가짜 저장소를 손쉽게 주입하여 단위 테스트 수행 MemberRepository mockRepo = new FakeMemberRepository(); MemberService service = new MemberService(mockRepo);
정리
- IoC: 객체의 생성, 생명주기, 의존성 제어권이 개발자에서 Spring 컨테이너로 역전되는 설계 개념입니다.
- DI: IoC 개념을 코드 수준에서 실현하는 구체적인 기술 패턴으로, 필요한 의존 객체를 외부에서 주입해 줍니다.
- 핵심 가치: 객체의 '생성'과 '사용' 책임을 명확히 분리하여 높은 유연성, 낮은 결합도, 뛰어난 테스트 용이성을 확보합니다.
다음 파트에서는 이렇게 Spring IoC/DI 컨테이너에 의해 생명주기가 관리되는 핵심 단위인 Spring Bean의 정의와 일반 자바 객체(POJO)와의 차이점을 살펴봅니다.
핵심 질문
Q1. IoC(Inversion of Control, 제어의 역전)란 무엇인가요?
객체의 생성, 의존성 연결, 생명주기 관리 등의 제어 주권이 개발자의 코드에서 프레임워크(Spring 컨테이너)로 넘어가는 설계 원칙을 의미합니다. 개발자는 객체 생성 제어에서 벗어나 비즈니스 로직 구현에만 집중할 수 있게 됩니다.
Q2. IoC와 DI의 차이점은 무엇인가요?
IoC는 제어권의 주체가 프레임워크로 이동한다는 상위 수준의 추상적인 설계 원칙/개념이며, DI는 이러한 IoC 원칙을 실제 코드 수준에서 달성하기 위해 외부에서 의존 객체를 주입해 주는 구체적인 기술 및 구현 패턴입니다.
Q3. 객체를 직접 new로 생성하지 않고 DI를 적용하는 이유는 무엇인가요?
객체의 생성 책임과 사용 책임을 분리하기 위함입니다. 이를 통해 객체 간 결합도를 낮추어 구현체 변경 시 유연한 대응이 가능해지며, 단위 테스트 시 가짜 객체(Mock)를 손쉽게 주입할 수 있어 테스트 용이성이 대폭 향상됩니다.
2.2 Spring Bean
앞서 다룬 IoC와 DI를 통해 Spring이 객체의 생성과 관리 주도권을 가져가고, 필요한 의존성을 자동으로 주입한다는 메커니즘을 살펴보았습니다.그렇다면 "Spring이 생성하고 관리하는 그 객체의 정체"는 무엇일까요? 이 객체들을 일컬어 Spring Bean(스프링 빈)이라고 부릅니다. 이후 학습할 DI, AOP, 선언적 트랜잭션, Spring MVC 등 Spring의 모든 핵심 기술은 예외 없이 이 Spring Bean을 중심으로 작동합니다.
Spring Bean이란 무엇인가?
Spring Bean은 Spring IoC 컨테이너가 생성, 의존성 주입, 생명주기를 직접 관리하는 객체를 의미합니다. 자바 애플리케이션에 존재하는 모든 객체가 Bean이 되는 것은 아닙니다. Spring 컨테이너의 관리 대상(Registry)으로 등록된 객체만을 Bean이라고 부릅니다.
// 1. 일반 Java 객체 (Spring이 알지 못함)
public class MemberService {
}
// 2. Spring Bean (@Service 어노테이션으로 스캔 및 컨테이너 등록 대상이 됨)
@Service
public class MemberService {
}
`@Service`, `@Controller`, `@Repository`, `@Component`, `@Bean` 등의 어노테이션이 부착되거나 설정 파일에 등록되어 애플리케이션 실행 시 컨테이너에 의해 생성된 객체가 바로 Spring Bean입니다.
일반 Java 객체 vs Spring Bean
두 객체 모두 형태적으로는 동일한 Java 클래스로 작성되지만, "객체를 관리하는 주체가 누구인가"에 따라 결정적인 차이가 발생합니다.
- 일반 Java 객체 (POJO)
- 관리 주체: 개발자 (`new` 키워드로 직접 생성)
- 생명주기: 개발자 작성 코드 및 GC(Garbage Collector)에 의해 관리
- 의존 관계: 개발자가 직접 코드로 연결 (`new` 및 setter/생성자 직접 호출)
- 특징: Spring이 제공하는 차원 높은 부가기능(AOP, Transaction 등)을 직접 받지 못함
- Spring Bean
- 관리 주체: Spring IoC 컨테이너
- 생명주기: 초기화부터 소멸까지 컨테이너가 제어
- 의존 관계: DI 메커니즘을 통해 컨테이너가 자동으로 주입 및 연결
- 특징: AOP, 트랜잭션 프록시, 보안, 이벤트 등 Spring 프레임워크 생태계의 모든 혜택을 받음
Spring이 Bean을 직접 관리하는 이유
Spring이 객체를 Bean으로 승격시켜 컨테이너에서 직접 관리하는 이유는 단순한 저장/보관 목적이 아닙니다.
Bean으로 관리되어야만 Spring Framework의 핵심 부가 기능들을 적용할 수 있기 때문입니다.
- 의존성 자동 주입 (DI): 필요한 객체를 자동으로 찾아 연결
- 싱글톤 스코프 관리: 객체를 애플리케이션 스코프 내에서 효율적으로 공유
- 생명주기 콜백 지원: 객체 생성 직후(`@PostConstruct`) 및 소멸 직전(`@PreDestroy`)에 원하는 로직 수행
- AOP 및 트랜잭션 프록시 적용: `@Transactional` 등의 어노테이션은 Spring이 Bean 객체 주위에 생성한 프레임워크 프록시(Proxy)를 통해서만 작동합니다. 일반 자바 객체에는 이러한 프록시가 적용되지 않습니다.
Bean은 언제 생성되는가?
기본 설정 상태인 Singleton Scope Bean은 애플리케이션 구동 시점(컨테이너 초기화 단계)에 모두 일괄 생성됩니다.
- Application 실행
- ApplicationContext(컨테이너) 생성
- Component Scan & 모든 Singleton Bean 생성
- Bean 간의 의존성 주입(DI)
- 애플리케이션 초기화 완료 및 요청 수신
애플리케이션이 외부 HTTP 요청을 받기도 전에, 필요한 모든 Bean을 미리 생성하고 의존 관계를 완성해 두기 때문에 실제 사용자 요청 처리 속도가 대폭 향상됩니다. (단, `@Lazy` 어노테이션을 부착하면 실제 사용 시점까지 객체 생성을 미룰 수도 있습니다.)
Bean 관리 구조 (이름과 타입)
Spring 컨테이너는 Bean을 등록할 때 Bean 이름(Name)과 Bean 타입(Type)이라는 두 가지 핵심 키 값을 기준으로 관리합니다.
@Service
public class MemberService { ... }
- 기본 Bean 이름: 클래스명의 첫 글자를 소문자로 바꾼 memberService (카멜 케이스 규칙)
- Bean 타입: 해당 클래스 타입(MemberService)은 물론, 구현한 인터페이스 타입으로도 함께 관리됩니다.
public interface MemberRepository { }
@Repository
public class JpaMemberRepository implements MemberRepository { }
위의 JpaMemberRepository Bean은 컨테이너 내부에서 아래의 두 가지 타입 모두로 조회 및 주입이 가능합니다.
- JpaMemberRepository (구체 클래스 타입)
- MemberRepository (인터페이스 타입)
Spring이 인터페이스 기반의 다형성 DI를 손쉽게 수행할 수 있는 원리가 바로 여기에 있습니다.
Bean과 일반 객체의 포함 관계

- 모든 Bean은 자바 객체이지만, 애플리케이션 안의 모든 자바 객체가 Bean인 것은 아닙니다.
- Spring Bean은 "Spring 컨테이너라는 특수한 공간에 등록되어 관리를 받는 자바 객체"입니다.
정리
- Spring Bean: Spring IoC 컨테이너가 생성, 의존성 주입, 생명주기를 총괄 관리하는 자바 객체입니다.
- 핵심 차이: 객체의 생성 주체가 개발자인지, 아니면 Spring 프레임워크(컨테이너)인지에 따라 나뉩니다.
- 존재 이유: Bean으로 관리되어야만 DI, AOP, 선언적 트랜잭션, 생명주기 콜백 등 Spring의 고차원적 기능들을 원활히 적용받을 수 있습니다.
다음 파트에서는 이러한 Bean들을 실제로 등록하고 내부에 저장하여 관리하는 핵심 주체인 Spring 컨테이너(BeanFactory 및 ApplicationContext)에 대해 다룹니다.
핵심 질문
Q1. Spring Bean이란 무엇인가요?
Spring IoC 컨테이너에 의해 생성, 의존성 주입, 생명주기가 관리되는 자바 객체를 의미합니다. 컨테이너에 등록된 객체만 Spring Bean이라 부릅니다.
Q2. 일반 Java 객체와 Spring Bean의 결정적인 차이는 무엇인가요?
객체의 관리 주체(Control)입니다. 일반 자바 객체는 개발자가 직접 `new`로 생성하고 생명주기를 제어하지만, Spring Bean은 Spring 컨테이너가 생성을 전담하고 DI, AOP, 트랜잭션 처리 등 프레임워크 부가기능을 적용해 줍니다.
Q3. 자바 객체를 그냥 쓰지 않고, 굳이 Spring Bean으로 등록하여 관리하는 이유는 무엇인가요?
객체를 컨테이너의 Bean으로 등록해야만 의존성 자동 주입(DI), 싱글톤 관리, 생명주기 콜백, 그리고 AOP 기반의 트랜잭션 프록시 처리 같은 Spring의 핵심 이점들을 누릴 수 있기 때문입니다.
Q4. Spring Bean의 기본 생성 시점은 언제인가요?
기본적으로 Singleton Scope를 따르므로, 애플리케이션이 구동되는 시점(컨테이너 초기화 단계)에 필요한 모든 Bean이 일괄 생성되고 의존성이 연결됩니다.
2.4 Spring 컨테이너 (BeanFactory와 ApplicationContext)
앞서 Spring Bean이 Spring IoC 컨테이너에 의해 생성되고 관리되는 객체임을 확인했습니다. 그렇다면 이 Spring Bean을 내부에 저장하고, 생명주기를 총괄하며 관리하는 그 컨테이너의 실체는 무엇일까요? Spring Framework에서는 객체의 생성, 연결, 관리를 전담하는 핵심 구성 요소를 Spring 컨테이너(Spring Container)라고 부르며, 자바 코드 수준에서는 BeanFactory와 ApplicationContext 두 인터페이스로 구현됩니다.
Spring 컨테이너의 역할과 구동 흐름
Spring 컨테이너는 애플리케이션 시작 시점에 설정 정보(어노테이션, 자바 설정 클래스 등)를 읽어와 Bean을 생성하고 저장 공간을 구축합니다.
- 설정 정보 읽기(Config Reader)
- Spring Container 생성 및 초기화
- Spring Bean 객체 생성
- 의존 관계 주입(DI) 연결
- 애플리케이션 실행 준비 완료(Ready)
Spring 애플리케이션은 사실상 이 컨테이너가 중심축이 되어 모든 객체의 생명주기를 주도하고 관리합니다.
BeanFactory
BeanFactory는 Spring 컨테이너의 최상위에 위치하는 가장 기초적인 기본 인터페이스입니다.
- 핵심 역할: Bean 생성, Bean 저장, Bean 조회(`getBean()`), 의존 관계 관리
- 특징: Bean을 관리하기 위한 최소한의 경량 기능만을 정의합니다.
BeanFactory beanFactory = ...;
// 컨테이너가 관리하는 Bean을 직접 조회
MemberService memberService = beanFactory.getBean(MemberService.class);
이처럼 BeanFactory는 순수하게 "Bean을 관리하고 제공하는 일"에만 집중된 단순 컨테이너 인터페이스입니다.
ApplicationContext
실무 및 모든 상용 프로젝트에서 이야기하는 Spring 컨테이너는 99% 이상 ApplicationContext를 의미합니다. ApplicationContext는 BeanFactory 인터페이스를 상속받아 그 기능을 모두 포함하면서, 엔터프라이즈 애플리케이션 개발에 필요한 다채로운 부가 기능들을 추가로 확장한 인터페이스입니다.

ApplicationContext가 추가로 제공하는 엔터프라이즈 기능
ApplicationContext는 단순히 Bean 관리 기능만 제공하는 것에 그치지 않고, 다음과 같은 강력한 부가 기능들을 함께 지니고 있습니다.
- 국제화 및 다국어 지원 (MessageSource): 국가 및 언어 설정에 따른 메시지 처리
- 환경 변수 구분 관리 (EnvironmentCapable): 로컬, 개발, 운영 등 프로파일(Profile)별 설정 및 프로퍼티 분리
- 이벤트 발행 및 구독 (ApplicationEventPublisher): 애플리케이션 내부 구성 요소 간의 컴포넌트 이벤트 처리 지원
- 리소스 조회 단순화 (ResourceLoader): 파일, 클래스패스, URL 등의 리소스를 일관되게 읽어오는 기능
- AOP 및 Bean 후처리기 자동 등록: 프록시 객체 생성 등 고차원적 기능 지원
왜 실무에서는 ApplicationContext를 사용하는가?
이론적으로는 BeanFactory만으로도 객체를 생성하고 DI를 수행할 수 있습니다.
하지만 실제 애플리케이션 환경에서는 다음과 같은 기능들이 필연적으로 요구됩니다.
- 다국어 지원이 필요한 서비스 → MessageSource
- 개발/운영 환경 구분이 필요한 서비스 → Environment Profile
- 이벤트 기반 비동기/decoupled 로직 처리 → ApplicationEventPublisher
- 외부 설정 파일이나 템플릿 로딩 → ResourceLoader
이 모든 부가 기능이 ApplicationContext 하나에 이미 녹아있기 때문에,
실무에서는 BeanFactory를 직접 사용하는 일이 없으며 ApplicationContext를 표준 컨테이너로 활용합니다.
Spring Boot와 ApplicationContext
Spring Boot 환경에서도 애플리케이션 구동 시 내부적으로 ApplicationContext를 생성하여 동작시킵니다.
@SpringBootApplication
public class Application {
public static void main(String[] args) {
// 내부적으로 ApplicationContext(컨테이너)를 생성 및 초기화함
SpringApplication.run(Application.class, args);
}
}
`SpringApplication.run()` 메서드가 실행되면 적절한 ApplicationContext 구현체(예: 웹 환경인 경우 `AnnotationConfigServletWebServerApplicationContext`)를 자동으로 띄워 Bean을 등록하고 의존성을 주입합니다.
Bean 조회와 관리의 실체
모든 Bean은 ApplicationContext 내부의 Bean Registry(저장소)에 보관됩니다.
// 직접 컨테이너에서 Bean을 꺼내오는 방식 (지양 / 단순 검증용)
MemberService service = applicationContext.getBean(MemberService.class);
실무 개발에서는 위와 같이 `applicationContext.getBean()`을 직접 호출하는 코드를 작성할 일이 거의 없습니다. Spring이 생성자 주입(DI)을 통해 필요한 Bean을 인젝션해 주므로, 개발자는 컨테이너의 존재조차 신경 쓰지 않고 자바 코드를 작성할 수 있습니다.
정리
- Spring 컨테이너: Spring Bean의 생성, 저장, DI, 생명주기를 다루는 핵심 주체입니다.
- BeanFactory: Bean 생성 및 조회라는 가장 기본적인 기능만을 정의한 최상위 인터페이스입니다.
- ApplicationContext: BeanFactory를 상속받아 국제화, 환경 변수, 이벤트, 리소스 처리 등 다양한 부가 기능을 확정한 실무용 표준 컨테이너입니다.
- Spring Boot: 구동 시점에 ApplicationContext를 자동으로 구성하여 모든 Bean과 프레임워크 기능을 구동시킵니다.
다음 파트에서는 Spring 컨테이너에 Bean을 등록하는 핵심 방법인 컴포넌트 스캔(Component Scan)과 자바 설정 파일(@Bean) 방식을 알아봅니다.
핵심 질문
Q1. Spring 컨테이너란 무엇인가요?
Spring Bean의 생성, 저장, 의존성 주입(DI), 그리고 생명주기를 총괄 관리하는 핵심 주체입니다. 대표적인 인터페이스로 BeanFactory와 ApplicationContext가 있습니다.
Q2. BeanFactory와 ApplicationContext의 차이는 무엇인가요?
BeanFactory는 Bean 관리 및 조회라는 최소한의 기능만 제공하는 상위 인터페이스입니다. ApplicationContext는 BeanFactory를 상속받아 Bean 관리 기능은 물론, 국제화(MessageSource), 환경 변수 분리(Environment), 이벤트 처리, 리소스 로딩 등 애플리케이션 구축에 필수적인 부가 기능을 통합 제공하는 실무 표준 컨테이너입니다.
Q3. 실무에서 BeanFactory 대신 ApplicationContext를 사용하는 이유는 무엇인가요?
단순한 Bean 관리 외에도 엔터프라이즈 환경 구축에 필수적인 국제화, 환경 프로파일 관리, 이벤트 발행, AOP 자동 처리 등 다양한 부가 기능이 통합 지원되기 때문입니다.
Q4. Spring Boot 애플리케이션에서도 Spring 컨테이너가 작동하나요?
네, 그렇습니다. SpringApplication.run()을 실행하면 내부적으로 웹 환경에 맞는 ApplicationContext를 스스로 생성하여 모든 Bean을 등록하고 관리합니다.
2.5 Bean 등록 방법 (자동 등록과 수동 등록)
앞서 Spring Bean이 Spring IoC 컨테이너에 의해 생성되고 관리되는 객체임을 살펴보았습니다. 그렇다면 Spring은 애플리케이션 내의 수많은 클래스 중 "어떤 객체를 Bean으로 등록해야 하는지" 어떻게 식별할까요? Spring은 크게 두 가지 접근 방식으로 Bean을 등록합니다.
- 자동 등록 (Component Scan): 어노테이션 기반으로 Spring이 알아서 찾아서 등록하는 방식
- 수동 등록 (@Bean): 자바 설정 파일에서 개발자가 직접 코드로 등록하는 방식
실무에서는 애플리케이션의 성격과 객체의 역할에 따라 두 방식을 적재적소에 혼용하여 사용합니다.
Bean 등록이 필요한 이유
Spring Framework는 프로젝트 내의 모든 자바 객체를 무분별하게 관리하지 않습니다.
미리 설정된 규칙에 따라 Spring 컨테이너에 Bean으로 등록된 객체만이 다음과 같은 Framework 특권을 누릴 수 있습니다.
- 의존성 자동 주입 (DI)
- 컨테이너 기반 생명주기(Lifecycle) 관리
- AOP 및 @Transactional 프록시 적용
- 애플리케이션 이벤트 발행 및 수신
즉, 객체를 Bean으로 올바르게 등록하는 것은 Spring의 생태계 안에서 작동시키기 위한 필수 첫 단추입니다.
1. 자동 등록 (Component Scan)
Spring이 특정 패키지 하위를 탐색(Scan)하면서 Bean으로 등록할 클래스를 자동으로 찾아 등록하는 방식입니다. 이를 컴포넌트 스캔(Component Scan)이라고 부릅니다. `@Component` 및 이를 내포한 스테레오타입 어노테이션이 붙은 클래스는 모두 자동 등록 대상이 됩니다.
@Controller
public class MemberController { ... }
@Service
public class MemberService { ... }
@Repository
public class JpaMemberRepository implements MemberRepository { ... }
애플리케이션 구동 시 Spring은 지정된 패키지 경로를 스캔하여 해당 클래스들의 인스턴스를 자동으로 생성하고 컨테이너에 Bean으로 등록합니다. 개발자가 직접 new를 호출하거나 별도의 설정 코드를 작성할 필요가 없습니다.
`@Component` 계열 어노테이션
자동 등록의 근간이 되는 핵심 어노테이션은 `@Component`입니다.
- `@Component`: 컴포넌트 스캔의 기본 대상
- `@Controller`: Spring MVC 컨트롤러로 인식 및 웹 요청 매핑 기능 제공
- `@Service`: 비즈니스 로직 계층임을 명시 (별도 계층 표식 역할)
- `@Repository`: 데이터 접근 계층임을 명시하며, 데이터 접근 예외를 Spring의 공통 예외로 변환
- `@Configuration`: Spring 설정 정보 클래스임을 명시하며, CGLIB 기반 싱글톤 프록시 보장
`@Controller`, `@Service`, `@Repository`, `@Configuration`은 모두 내부적으로 `@Component`를 포함(Meta-Annotation)하고 있으므로 Component Scan에 의해 자동으로 수집됩니다.
2. 수동 등록 (`@Configuration`과 `@Bean`)
개발자가 자바 설정 클래스를 작성하고, 메서드 상단에 @Bean 어노테이션을 부착하여 직접 Bean 객체를 생성 및 반환하는 방식입니다.
@Configuration
public class AppConfig {
@Bean
public MemberRepository memberRepository() {
return new JpaMemberRepository();
}
@Bean
public MemberService memberService(MemberRepository repository) {
return new MemberService(repository);
}
}
- @Configuration: 이 클래스가 Spring의 설정 정보(Config)를 담고 있는 클래스임을 컨테이너에 알립니다.
- @Bean: 해당 메서드가 반환하는 객체를 Spring 컨테이너의 Bean으로 등록합니다. (기본 Bean 이름은 메서드명이 됩니다.)
이 방식은 객체 생성 과정, 초기화 로직, 의존성 연결 방식을 개발자가 자바 코드로 세밀하게 제어할 수 있다는 특징이 있습니다.
자동 등록 vs 수동 등록 비교
두 방식 모두 결과적으로 Spring 컨테이너 내부의 Bean 저장소에 일관되게 보관되며,
완료 이후에는 DI, AOP, 트랜잭션 등 프레임워크 기능 적용에 아무런 차이가 없습니다.
| 구분 | 자동 등록 (`@Component`) | 수동 등록 (`@Bean`) |
| 등록 주체 | Spring 컨테이너 (자동 스캔) | 개발자 (자바 설정 코드로 명시) |
| 사용 어노테이션 | `@Component`, `@Service`, `@Repository` 등 | `@Configuration`, `@Bean` |
| 생성 과정 제어 | 기본 생성자 또는 DI에 의존 | 객체 생성 및 설정을 코드로 직접 구성 가능 |
| 주된 사용 대상 | 직접 작성하는 계층별 클래스 (Controller, Service 등) | 외부 라이브러리 객체, 공통 설정 객체, 복잡한 인스턴스 |
| 관리 편의성 | 클래스 생성 후 어노테이션만 붙이면 되어 매우 편리함 | 설정 파일 관리가 필요하나 명시적 파악이 용이함 |
각각 언제 사용하는가?
자동 등록을 사용하는 경우
우리가 프로젝트에서 직접 작성하는 일반적인 비즈니스 애플리케이션 코드는 예외 없이 자동 등록을 기본으로 사용합니다.
- Controller / Service / Repository / 일반 Component
- 계층 구조가 명확하고 수량이 많은 도메인 로직 관련 클래스들은 컴포넌트 스캔을 통해 관리하는 것이 관리 포인트를 혁신적으로 줄여줍니다.
수동 등록을 사용하는 경우
직접 작성한 코드가 아니거나, 생성 과정에 세밀한 개입이 필요한 경우에는 수동 등록이 적합합니다.
- 외부 라이브러리 객체 등록
외부 라이브러리 클래스(예: Jackson의 ObjectMapper)는 소스 코드를 수정할 수 없으므로 `@Component`를 붙일 수 없습니다.
@Bean public ObjectMapper objectMapper() { return new ObjectMapper(); // 외부 라이브러리 객체를 Bean으로 수동 등록 } - 생성 과정 및 초기화 설정이 복잡한 객체
다양한 옵션 세팅이나 사전 작업이 필요한 경우 자바 코드로 깔끔하게 구성을 완성하여 반환할 수 있습니다.
@Bean public RestTemplate restTemplate() { RestTemplate restTemplate = new RestTemplate(); // 타임아웃, 인터셉터 등 복잡한 설정 추가 return restTemplate; } - 다형성 활용 시 구현체를 명시적으로 관리하고 싶은 경우
상황이나 환경 설정에 따라 특정 구현체를 직관적으로 교체하고 싶을 때 설정 파일 한 곳에서 변경 사항을 조율할 수 있습니다.
실무에서의 전략적 선택 기준

Spring Boot 환경에서는 컴포넌트 스캔이 기본 설정으로 작동합니다. 따라서 "직접 작성하는 비즈니스 코드는 자동 등록, 외부 라이브러리 및 공통 기술 설정 객체는 수동 등록"을 표준 원칙으로 삼으면 명확하고 깔끔한 구성이 완성됩니다.
정리
- Bean 등록 메커니즘: @Component 계열 기반의 자동 등록과 `@Configuration` + `@Bean` 기반의 수동 등록 두 가지 방식이 있습니다.
- 등록 후 동작: 수동이든 자동이든 일단 컨테이너에 등록된 이후에는 동등하게 Spring 프레임워크의 생명주기 및 부가기능 관리를 받습니다.
- 실무 원칙: 업무 로직 클래스는 자동 등록으로 생산성을 높이고, 기술적인 설정이나 외부 라이브러리는 수동 등록으로 명시적 관리와 제어권을 가져갑니다.
다음 파트에서는 자동 등록의 핵심 축인 Component Scan이 애플리케이션 시작 시점에 내부적으로 어떤 순서와 원리로 동작하는지 상세히 살펴봅니다.
핵심 질문
Q1. Spring에서 Bean을 등록하는 대표적인 두 가지 방식은 무엇인가요?
컴포넌트 스캔을 통한 자동 등록(`@Component`, `@Service`, `@Repository` 등)과 자바 설정 클래스를 통한 수동 등록(`@Configuration` + `@Bean`) 두 가지 방식이 있습니다.
Q2. @Component와 @Bean의 차이점은 무엇인가요?
@Component는 개발자가 직접 작성한 클래스 단위에 선언하여 컴포넌트 스캔을 통해 자동으로 Bean에 등록되도록 하는 어노테이션입니다. 반면 `@Bean`은 설정 클래스(`@Configuration`) 내의 메서드 단위에 선언하여, 외부 라이브러리 객체나 복잡한 생성 로직을 가진 객체를 개발자가 직접 코드로 반환하여 등록할 때 사용합니다.
Q3. 실무에서는 자동 등록과 수동 등록을 어떤 기준으로 구분하여 사용하나요?
컨트롤러, 서비스, 리포지토리 등 직접 작성하는 비즈니스 애플리케이션 코드는 생산성을 위해 자동 등록(`@Component`)을 기본으로 사용합니다. 반면 소스 코드를 수정할 수 없는 외부 라이브러리 객체, 생성 로직이 복잡한 기술 지원 객체, 또는 공통 환경 설정 객체는 수동 등록(`@Bean`)을 사용하여 명시적으로 관리합니다.
Q4. 수동으로 등록한 Bean과 자동으로 등록된 Bean은 컨테이너 내에서 다르게 동작하나요?
아니오, 다르게 동작하지 않습니다. 등록 경로의 차이일 뿐 컨테이너에 등록된 이후에는 모두 동일한 Spring Bean으로 관리되며, DI, 생명주기 콜백, AOP 및 트랜잭션 적용 등 모든 프레임워크 기능을 동일하게 제공받습니다.
2.6 Component Scan (컴포넌트 스캔)
앞서 Spring Bean을 등록하는 방법에는 자동 등록과 수동 등록 두 가지가 있음을 살펴보았습니다. 이 중 개발자가 일일이 설정 코드를 작성하지 않아도 Bean 자동 등록을 가능하게 만드는 Spring의 핵심 메커니즘이 바로 컴포넌트 스캔(Component Scan)입니다. Spring Boot 프로젝트에서 별도의 설정 없이 클래스 상단에 `@Service`, `@Repository`, `@Controller`만 붙여도 알아서 Bean으로 등록 및 관리되는 이유가 바로 이 Component Scan 덕분입니다.
Component Scan이란?
Component Scan은 지정된 패키지 경로 및 하위 패키지를 탐색하면서 Bean으로 등록할 대상 클래스들을 자동으로 찾아 식별하는 기능입니다. Spring은 구동 시점에 클래스에 붙어 있는 특정 어노테이션을 확인하고, 해당 메타데이터를 기반으로 객체를 생성하여 Spring IoC 컨테이너의 Bean으로 수집합니다.
@Service
public class MemberService {
// 개발자가 직접 AppConfig에 new MemberService()를 작성하지 않아도
// Component Scan에 의해 Spring Bean으로 자동 등록됨
}
Component Scan의 전체 동작 과정
Component Scan은 애플리케이션 시작 시점에 다음과 같은 순서로 실행되어 Bean 등록과 의존성 주입까지 완성합니다.
- Application 시작 (`@SpringBootApplication` 실행)
- Component Scan 탐색 개시 (기정 패키지 기준)
- 패키지 수집 및 `@Component` 계열 클래스 탐색
- Spring Bean 생성 및 IoC 컨테이너 등록
- 의존성 주입(Dependency Injection) 연쇄 수행
- 탐색 단계: 설정된 메인 패키지 하위의 모든 `.class` 파일을 스캔합니다.
- 식별 단계: `@Component` 또는 이를 내포한 어노테이션이 부착된 클래스를 선별합니다.
- 등록 단계: 선별된 클래스의 인스턴스를 생성하여 Spring 컨테이너의 Bean으로 등록합니다.
- 연결 단계: 등록된 Bean들 간의 의존 관계(DI)를 파악하고 필요한 주입을 처리합니다.
탐색 대상 어노테이션 (Stereotype Annotation)
Component Scan의 기본 스캔 대상은 `@Component`입니다. 하지만 실제 개발 시에는 `@Component` 외에도 역할과 계층을 명확히 나타내는 스테레오타입 어노테이션(Stereotype Annotation)을 사용합니다.
- `@Component`: 자동 등록의 가장 기본이 되는 어노테이션
- `@Controller`: Spring MVC 웹 요청 처리 계층
- `@Service`: 비즈니스 로직 계층
- `@Repository`: 데이터 접근 계층 (데이터베이스 예외 추상화 제공)
- `@Configuration`: Spring 설정 정보 계층 (CGLIB 프록시 적용)
스테레오타입 어노테이션의 내부 구조
이 어노테이션들이 Component Scan의 대상이 될 수 있는 이유는 내부 메타 어노테이션으로 `@Component`를 포함하고 있기 때문입니다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component // 메타 어노테이션으로 @Component를 포함
public @interface Service {
...
}
자바 언어 차원에서는 어노테이션의 상속이라는 개념이 없지만, Spring은 메타 어노테이션을 추적하여 `@Service`, `@Repository`, `@Controller` 등을 모두 `@Component`로 인식하고 Scan 대상에 포함시킵니다.
탐색 시작 위치 (Base Package)
Component Scan은 프로젝트나 JVM 내의 모든 클래스를 무작정 탐색하지 않습니다.
탐색 시간 효율과 오동작 방지를 위해 기준이 되는 시작 패키지가 필요합니다.
@SpringBootApplication // 내부적으로 @ComponentScan이 포함되어 있음
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Spring Boot에서는 `@SpringBootApplication`이 위치한 클래스의 패키지가 Component Scan의 시작 기준점(Base Package)이 됩니다.
패키지 구조 배치가 중요한 이유
만약 다음과 같은 프로젝트 구조가 있다고 가정해 봅시다.

- `@SpringBootApplication이 com.example` 루트 패키지에 위치하므로, 하위의 member, order, common 패키지 내의 모든 `@Component` 계열 클래스가 정상적으로 탐색됩니다.
- 만약 `Application.java`가 `com.example.member` 하위로 이동한다면, order나 common 패키지의 클래스들은 Scan 범위를 벗어나 Spring Bean으로 등록되지 않습니다.
따라서 Spring Boot 프로젝트에서는 메인 클래스를 항상 최상위(Root) 패키지에 위치시키는 것이 중요한 관례입니다.
Component Scan과 의존성 주입(DI)의 차이
두 개념은 같이 일하지만 명확히 분리된 역할을 수행합니다.
- Component Scan: 컨테이너에 올릴 Bean을 찾아서 등록하는 역할
- Dependency Injection (DI): 이미 등록된 Bean 간의 관계를 연결해 주는 역할
@Service
public class MemberService {
private final MemberRepository repository;
// 생성자 주입
public MemberService(MemberRepository repository) {
this.repository = repository;
}
}
@Repository
public class JpaMemberRepository implements MemberRepository { ... }
[1단계: Component Scan]
MemberService 클래스 발견 ──> MemberService Bean 등록
JpaMemberRepository 발견 ──> JpaMemberRepository Bean 등록
[2단계: Dependency Injection]
MemberService 생성자 확인 ──> 등록된 JpaMemberRepository Bean을 찾아 주입
Component Scan이 객체를 생성해 컨테이너에 먼저 채워놓아야, DI가 그 객체들을 찾아 서로 연결해 줄 수 있습니다.
Component Scan의 장점
- 반복적인 설정 코드 제거: 새로운 클래스를 만들 때마다 설정 파일(AppConfig)을 수정할 필요가 없어 생산성이 극대화됩니다.
- 코드 일관성 유지: 계층별 어노테이션 명시 규칙을 통해 프로젝트 전반의 구조가 일관되게 유지됩니다.
- Spring Boot와의 완벽한 조화: 별도의 복잡한 XML이나 자바 설정 없이 애플리케이션 개발에만 집중할 수 있습니다.
정리
- Component Scan 메커니즘: 지정된 패키지 경로 하위를 탐색하여 `@Component` 및 스테레오타입 어노테이션이 붙은 클래스를 찾아 자동으로 Bean에 등록합니다.
- 루트 패키지 관례: Spring Boot는 `@SpringBootApplication`이 위치한 최상위 패키지를 기준으로 하위 경로 전체를 탐색하므로 메인 클래스의 위치 설정이 매우 중요합니다.
- 역할 분담: Component Scan은 Bean의 등록을 담당하고, DI는 등록된 Bean 간의 연결을 담당합니다.
다음 파트에서는 `@Component`라는 하나의 어노테이션으로 통일할 수 있음에도 왜 `@Controller`, `@Service`, `@Repository`로 구분하여 사용하는지 각 어노테이션별 차이점과 부가 기능을 자세히 살펴봅니다.
핵심 질문
Q1. Component Scan이란 무엇인가요?
지정된 시작 패키지 및 하위 경로를 탐색하여 `@Component` 및 스테레오타입 어노테이션이 부착된 클래스들을 발굴하고, Spring IoC 컨테이너의 Bean으로 자동 등록해 주는 기능입니다.
Q2. `@Service`나 `@Repository`는 `@Component`가 아닌데 어떻게 Component Scan의 대상이 되나요?
`@Service`, `@Repository`, `@Controller` 등의 어노테이션 내부 메타 어노테이션으로 `@Component`가 포함되어 있기 때문입니다. Spring은 메타 어노테이션까지 추적하여 탐색 대상으로 인지합니다.
Q3. Component Scan과 Dependency Injection(DI)의 차이는 무엇인가요?
Component Scan은 탐색을 통해 객체를 생성하고 Spring Bean으로 등록하는 단계이며, DI는 등록된 Bean들 간의 의존 관계를 파악하여 주입해 주는 단계입니다. Scan이 선행되어야 DI가 정상 동작할 수 있습니다.
Q4. Spring Boot 메인 클래스를 프로젝트의 최상위 패키지에 두어야 하는 이유는 무엇인가요?
Spring Boot의 `@SpringBootApplication` 내부에 포함된 Component Scan은 기본적으로 메인 클래스가 속한 패키지를 기준점(Base Package)으로 하위 패키지를 탐색하기 때문입니다. 최상위에 두지 않으면 일부 패키지가 탐색 대상에서 누락되는 문제가 발생할 수 있습니다.
2.7 @Component, @Service, @Repository, @Controller의 차이
앞서 Component Scan은 @Component 계열의 어노테이션이 부착된 클래스를 탐색하여 자동으로 Spring Bean으로 등록한다는 것을 살펴보았습니다. 그렇다면 다음과 같은 의문이 생깁니다. "어차피 모두 @Component를 기반으로 동작하여 Bean으로 등록된다면, 왜 굳이 어노테이션을 여러 개로 나누어 사용할까요?" 결론부터 말하면 Bean 등록 메커니즘은 동일하지만, 클래스의 계층적 역할을 명확히 표현(가독성·유지보수성)하고 Spring Framework 차원의 추가 기능(예외 변환 등)을 제공하기 위해 구분합니다.
1. 모든 어노테이션의 뿌리는 `@Component`
다음 네 가지 스테레오타입 어노테이션은 모두 Component Scan의 대상입니다.
- `@Component`
- `@Controller`
- `@Service`
- `@Repository`
예를 들어 `@Service` 어노테이션의 내부 선언을 살펴보면 다음과 같습니다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component // 메타 어노테이션으로 @Component를 내포
public @interface Service {
...
}
따라서 아래 두 코드는 Spring IoC 컨테이너에 Bean으로 등록된다는 관점에서는 완전하게 동일합니다.
// 1. @Component 사용
@Component
public class MemberService { ... }
// 2. @Service 사용
@Service
public class MemberService { ... }
2. 어노테이션별 역할과 특징
`@Component` (기본 컴포넌트)
- 역할: 가장 기본이 되는 Bean 등록 어노테이션입니다.
- 사용 대상: 웹, 비즈니스, 데이터 접근 계층 등 명확한 아키텍처 레이어에 속하지 않는 공통 유틸리티나 기술 지원 객체에 주로 사용합니다.
- 예시: JwtProvider, FileStorageManager, RedisClientWrapper 등
`@Service` (비즈니스 계층)
- 역할: 비즈니스 로직 및 트랜잭션 경계를 담당하는 서비스 계층임을 명시합니다.
- 특징: 기술적으로 Spring이 제공하는 특별한 추가 기능은 없습니다. 하지만 해당 클래스가 순수한 핵심 도메인 로직을 처리하는 주체임을 개발자에게 명확하게 전달합니다.
`@Repository` (데이터 접근 계층)
- 역할: 데이터 베이스 접근 계층(DAO, Repository)임을 명시합니다.
- 특징 (추가 기능 제공): 단순한 역할 명시를 넘어 Spring의 예외 변환(Exception Translation) 기능이 적용됩니다.
- JPA, Hibernate, JDBC 등 persistence 기술에서 발생하는 고유한 예외(e.g., SQLException, PersistenceException)를 Spring의 공통 예외 체계인 DataAccessException 계층으로 추상화하여 변환해 줍니다.
- 이 덕분에 DB 접근 기술이 바뀌더라도 서비스 계층의 예외 처리 로직은 일관되게 유지할 수 있습니다.
`@Controller` & `@RestController` (표현 계층)
- `@Controller`: Spring MVC의 웹 요청을 수신하고 View(HTML)를 반환하는 컨트롤러 계층에 사용합니다.
- `@RestController`: RESTful API 웹 서비스를 구축할 때 사용하며, `@Controller` + `@ResponseBody`의 결합 형태입니다.
- 반환 객체가 View 페이지로 연결되지 않고, HTTP Response Body에 JSON/XML 형태로 직접 직렬화되어 응답됩니다.
3. 계층 구조와 어노테이션 매핑
Spring 애플리케이션은 아키텍처 관점에서 보통 다음과 같은 4계층(Layered Architecture) 구조를 따릅니다.

| 계층 (Layer) | 어노테이션 | 주요 역할 및 특징 |
| Presentation Layer | `@Controller`, `@RestController` |
HTTP 요청 매핑, 파라미터 검증, 응답 반환 (View 또는 JSON) |
| Business Layer | `@Service` | 핵심 비즈니스 로직 수행, 트랜잭션 경계 설정 (`@Transactional`) |
| Persistence Layer | `@Repository` | DB CRUD 작업, persistence 예외를 DataAccessException으로 추상화 변환 |
| Common / Utility | `@Component` | 특정 계층에 속하지 않는 유틸리티, 헬퍼 클래스, 기술 공통 객체 |
4. 실무에서의 선택 기준
실무에서는 클래스의 아키텍처 역할에 맞는 어노테이션을 사용하는 것을 엄격하게 권장합니다.
- 컨트롤러: `@RestController` (REST API 기준)
- 비즈니스 로직: `@Service`
- 데이터 접근: `@Repository`
- 공통 컴포넌트: `@Component`
모든 클래스에 `@Component`만 붙여도 기술적으로 동작에는 문제가 없습니다. 그러나 역할에 맞는 어노테이션을 쓰면 코드의 의미(Intent)가 명확해져 가독성과 유지보수성이 높아지며, `@Repository`의 예외 변환과 같이 Spring이 레이어별로 제공하는 부가 기능을 안전하게 누릴 수 있습니다.
정리
- 공통점: `@Component`, `@Service`, `@Repository`, `@Controller`는 모두 내부적으로 `@Component`를 포함하므로 Component Scan에 의해 스캔 및 자동 등록됩니다.
- 차이점: 계층별 의미 전달(가독성)과 명확한 의도 구분을 가능하게 하며, 특히 `@Repository`는 데이터 접근 예외 추상화 변환 기능, `@RestController`는 HTTP 응답 본문 직렬화 기능 등을 선언적으로 제공합니다.
- 실무 원칙: 코드 가독성, 예외 처리 구조화, 프레임워크 기능 활용을 위해 항상 계층에 부합하는 어노테이션을 선언하여 사용합니다.
핵심 질문
Q1. `@Component`와 `@Service`의 차이는 무엇인가요?
기술적으로 Spring Bean으로 등록되는 메커니즘은 동일합니다. 하지만 `@Service`는 해당 클래스가 비즈니스 로직을 처리하는 서비스 계층임을 명시적으로 나타내어 가독성을 높이고 아키텍처 레이어를 명확히 하는 역할을 합니다.
Q2. 모든 데이터 접근 클래스에 `@Component` 대신 `@Repository`를 붙여야 하는 기술적인 이유는 무엇인가요?
데이터 접근 계층임을 명시하는 것 외에도, Spring의 예외 변환(Exception Translation) AOP 기능이 적용되기 때문입니다. 이를 통해 JPA나 JDBC 등 하위 데이터베이스 접근 기술에서 발생한 기술 종속적 예외를 Spring의 공통 예외 체계인 `DataAccessException`으로 추상화하여 변환해 줍니다.
Q3. `@Controller`와 `@RestController`는 어떻게 다른가요?
`@Controller`는 전통적인 Spring MVC 어노테이션으로 메서드의 반환값을 View 이름으로 해석하여 HTML 화면을 렌더링합니다. 반면 `@RestController`는 `@Controller`에 `@ResponseBody`가 합성된 어노테이션으로, 반환 데이터를 View가 아닌 HTTP Response Body에 JSON/XML 형식으로 직접 직렬화하여 반환합니다.
Q4. 프로젝트의 모든 클래스에 @Component만 사용하면 어떻게 되나요?
애플리케이션 실행 및 Bean 등록 자체는 정상적으로 동작합니다. 하지만 각 클래스의 역할과 아키텍처 계층을 한눈에 파악하기 힘들어 유지보수성이 저하되며, `@Repository`가 제공하는 예외 변환 프록시 기능 등 계층 특화 부가 기능을 활용할 수 없게 됩니다.
'🍃SpringBoot' 카테고리의 다른 글
| Spring: Part 4. Bean 생명주기(Lifecycle) (1) | 2026.07.29 |
|---|---|
| Spring: Part 3. 의존성 주입(DI)과 활용 (0) | 2026.07.29 |
| Spring: Part 1. Spring Framework의 이해 (0) | 2026.07.28 |
| Spring IoC & DI 완전 정복: Chapter 8. Proxy와 Spring AOP (1) | 2026.07.22 |
| Spring IoC & DI 완전 정복: Chapter 7. BeanPostProcessor와 Spring의 확장 포인트 (0) | 2026.07.22 |
