티스토리 뷰
Spring 핵심 개념 정리 | IoC, DI, Bean, Container, ApplicationContext 쉽게 이해하기
PARK_90 2026. 4. 10. 15:04Spring을 처음 공부할 때 가장 먼저 막히는 지점이 바로 IoC, DI, Bean, Container입니다. 용어는 계속 나오는데 각각이 정확히 무엇이고, 서로 어떤 관계인지 한 번에 정리되지 않아서 개념이 흐릿하게 남는 경우가 많습니다.
특히 Spring Boot 예제를 따라 치다 보면 @Component, @Service, @Autowired, ApplicationContext 같은 키워드는 자주 보이는데, 왜 이런 구조를 쓰는지 이해하지 못하면 나중에 설정이나 에러를 만났을 때 훨씬 더 헷갈리게 됩니다.
결론부터 말하면 Spring의 핵심은 객체를 내가 직접 만들고 연결하던 방식을 프레임워크가 대신 관리하게 바꾸는 것입니다. 이 글에서는 Spring 핵심 개념을 쉬운 정의부터 구조, 예제, 자주 막히는 포인트, 실무에서 꼭 보는 체크포인트까지 한 번에 정리하겠습니다.
- IoC와 DI는 무엇이 다를까?
- Bean은 그냥 객체와 무엇이 다를까?
- Spring Container와 ApplicationContext는 어떤 역할을 할까?
- 왜 초보자가 @Autowired, 컴포넌트 스캔, Bean 등록에서 자주 막힐까?
Spring은 객체를 만들고, 보관하고, 서로 연결하는 일을 프레임워크가 대신 관리해주는 구조라고 이해하면 가장 쉽습니다.
예전에는 개발자가 직접 new로 객체를 만들고, 필요한 객체를 생성자나 setter로 하나하나 연결해야 했습니다. 그런데 서비스가 커질수록 객체 간 의존관계가 복잡해지고, 테스트와 변경도 어려워집니다.
Spring은 이 문제를 줄이기 위해 객체 생성과 관리의 주도권을 가져가고, 필요한 객체를 알맞게 넣어주는 구조를 제공합니다. 여기서 등장하는 핵심 개념이 바로 IoC, DI, Bean, Container입니다.
Spring을 한 줄로 요약하면 “객체를 내가 직접 관리하던 방식을 Spring Container가 대신 관리하도록 바꾸는 것”입니다.
Spring Boot를 쓰면 기능이 자동으로 되는 것처럼 보여도, 실제로는 내부에서 Bean 등록, 의존관계 주입, 설정 클래스 로딩, 컴포넌트 스캔이 계속 일어나고 있습니다.
그래서 아래 상황에서 거의 반드시 이 개념들이 다시 등장합니다.
@Service,@Repository,@Controller가 왜 Bean이 되는지 이해할 때@Autowired가 왜 동작하는지 이해할 때- 생성자 주입을 왜 권장하는지 이해할 때
NoSuchBeanDefinitionException,NoUniqueBeanDefinitionException같은 오류를 해결할 때- 설정 클래스로 수동 Bean 등록을 할 때
- 테스트에서 Mock Bean이나 설정 분리를 할 때
Spring은 단순히 “라이브러리 모음”이 아니라 객체 생명주기와 의존관계를 관리하는 컨테이너 기반 프레임워크입니다. 이 관점을 놓치면 어노테이션만 외우게 되고 구조는 남지 않습니다.
먼저 Spring 없이 직접 객체를 만들면 보통 이런 식입니다.
public class OrderService {
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
public int calculateDiscount(int price) {
return discountPolicy.discount(price);
}
}
이 방식은 간단해 보이지만, OrderService가 어떤 할인 정책을 쓸지 스스로 결정하고 있습니다. 즉 객체 생성과 사용이 강하게 묶여 있습니다.
Spring DI 구조로 바꾸면 이렇게 됩니다.
@Component
public class FixDiscountPolicy implements DiscountPolicy {
@Override
public int discount(int price) {
return 1000;
}
}
@Service
public class OrderService {
private final DiscountPolicy discountPolicy;
public OrderService(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
public int calculateDiscount(int price) {
return discountPolicy.discount(price);
}
}
이제 OrderService는 어떤 구현체가 들어오는지 직접 만들지 않습니다. 필요한 객체를 생성자 주입으로 받기만 합니다.
즉 핵심 차이는 아래입니다.
- 직접 생성 방식: 사용하는 클래스가 의존 객체를 스스로 생성
- DI 방식: 사용하는 클래스는 필요한 타입만 선언하고, 실제 연결은 Spring이 담당
직접 생성 방식
OrderService
└─ new FixDiscountPolicy()
Spring DI 방식
Spring Container
├─ OrderService Bean 생성
└─ FixDiscountPolicy Bean 생성 후 주입
Spring Container를 설명할 때 BeanFactory와 ApplicationContext가 같이 나옵니다.
실무에서는 보통 Spring Container = 거의 ApplicationContext 중심으로 이해해도 큰 무리는 없습니다. BeanFactory는 기본 개념의 뿌리이고, 실제 애플리케이션에서는 ApplicationContext가 더 자주 등장합니다.
BeanFactory는 “Bean 관리의 기본형”, ApplicationContext는 “실무에서 쓰는 확장형 Container”라고 정리하면 됩니다.
아래처럼 ApplicationContext에서 Bean을 조회해보면, Spring Container가 객체를 직접 관리하고 있다는 점이 더 명확하게 보입니다.
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
OrderService orderService = context.getBean(OrderService.class);
DiscountPolicy discountPolicy = context.getBean(DiscountPolicy.class);
이 코드는 단순히 객체를 생성하는 코드가 아니라, Spring Container 안에 이미 등록된 Bean을 꺼내오는 코드입니다. 즉 BeanFactory와 ApplicationContext는 단순한 개념 설명에서 끝나는 것이 아니라, 실제로 Bean을 등록하고 조회하는 실행 주체라고 이해하면 됩니다.
- 클래스에
@Component,@Service,@Repository,@Controller또는@Bean등록이 되어 있는가? - 메인 애플리케이션 클래스 기준 하위 패키지에 위치해 있는가?
- 같은 타입 Bean이 여러 개인데
@Qualifier또는@Primary처리가 필요한가? - 직접
new로 객체를 생성해서 Spring 관리 밖으로 빼버리진 않았는가? - 필드 주입보다 생성자 주입으로 구조를 단순하게 가져가고 있는가?
예를 들어 같은 타입의 구현체가 둘 다 Bean으로 등록돼 있으면 아래처럼 주입 시점에 충돌할 수 있습니다.
@Component
public class FixDiscountPolicy implements DiscountPolicy {
}
@Component
public class RateDiscountPolicy implements DiscountPolicy {
}
@Service
public class OrderService {
private final DiscountPolicy discountPolicy;
public OrderService(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}
이 경우 Spring은 DiscountPolicy 타입 Bean이 두 개라서 어떤 구현체를 넣어야 할지 결정하지 못할 수 있습니다. 그래서 @Qualifier나 @Primary가 왜 필요한지도 이 코드에서 자연스럽게 이해할 수 있습니다.
- 생성자 주입을 기본값으로 가져가면 필수 의존성이 명확해지고 테스트하기 쉬워집니다.
- 구현체보다 인터페이스에 의존하도록 설계하면 교체와 확장이 쉬워집니다.
- 필드 주입은 편해 보여도 테스트, 불변성, 명시성 측면에서 생성자 주입보다 불리한 경우가 많습니다.
- Spring이 관리해야 할 객체와 그렇지 않은 객체를 구분하는 감각이 중요합니다. 모든 클래스를 Bean으로 만들 필요는 없습니다.
- IoC와 DI는 같은 말이 아니라, 제어권 변화와 주입 방식이라는 차이가 있습니다.
- Bean은 그냥 객체가 아니라 Spring Container가 관리하는 객체입니다.
- Container를 이해하면
@Autowired, 컴포넌트 스캔, 설정 클래스가 한 흐름으로 보입니다. - ApplicationContext는 실무에서 가장 자주 접하는 Spring Container입니다.
Bean을 등록하는 대표 방식은 크게 두 가지입니다.
즉 대부분의 비즈니스 로직 클래스는 컴포넌트 스캔으로 등록하고, 세밀한 제어가 필요한 설정성 객체는 @Bean으로 등록한다고 이해하면 실무 감각에 가깝습니다.
- Q. IoC와 DI는 같은 말인가요?
→ 완전히 같은 말은 아닙니다. IoC는 제어권이 Spring으로 넘어가는 큰 개념이고, DI는 그 안에서 의존 객체를 주입하는 대표 방식입니다. - Q. Bean은 그냥 객체와 무엇이 다른가요?
→ Bean은 Spring Container가 생성하고 관리하는 객체입니다. 그냥new로 만든 객체는 Spring이 관리하지 않으면 Bean이 아닙니다. - Q. ApplicationContext와 Container는 같은 말로 봐도 되나요?
→ 실무에서는 거의 비슷한 의미로 써도 큰 무리는 없지만, 정확히는 ApplicationContext가 대표적인 Spring Container 구현체입니다. - Q. 왜 생성자 주입을 많이 쓰나요?
→ 필수 의존성을 명확히 드러내고, 불변성 유지와 테스트 작성에 유리하기 때문입니다.
Spring 핵심 개념 정리에서 가장 중요한 건 용어를 각각 암기하는 것이 아니라, 객체 생성과 연결의 주체가 누구인지를 분명하게 이해하는 것입니다.
- IoC는 객체 관리의 주도권이 Spring으로 넘어가는 개념
- DI는 필요한 객체를 외부에서 주입받는 방식
- Bean은 Spring이 관리하는 객체
- Container는 Bean을 생성·보관·연결하는 공간
- ApplicationContext는 실무에서 가장 자주 보는 Spring Container
Spring을 제대로 이해하는 출발점은 “내가 객체를 관리하는가, 아니면 Spring이 관리하는가”를 구분하는 데서 시작합니다.
'IT > Spring' 카테고리의 다른 글
| @Transactional이란 무엇인가? | 롤백, readOnly, 주의점까지 실무 기준으로 이해하기 (0) | 2026.04.12 |
|---|---|
| Spring MVC 구조 정리 | Controller, Service, Repository 역할과 흐름 이해하기 (0) | 2026.04.10 |
| Spring Boot JWT 로그인 구현 방법 | Spring Security 설정부터 Access Token·Refresh Token 발급·검증까지 (0) | 2026.04.02 |
| [Spring Boot] GET, POST, PUT, PATCH, DELETE 차이와 CRUD 구현 방법 (0) | 2026.03.20 |
| Spring Framework와 Spring Boot 차이 | 개념, 구조, 역할 한 번에 이해하기 (0) | 2022.09.16 |
