스프링을 처음 배우면 @Autowired, @Component 같은 애너테이션이 먼저 보이는데, 그걸 외우기 시작하면 금방 막힌다.
핵심은 애너테이션이 아니라 “new를 누가 하느냐”다.
- 내가 new를 하면 → 결합이 강해지고 테스트가 어려워진다.
- 스프링이 new를 해주면 → 결합이 약해지고 교체/테스트가 쉬워진다.
이 글은 IoC/DI/Bean을 한 세트로 이해시키는 게 목표다.
1. new 지옥: “직접 만들고 직접 연결하는 방식”
예를 들어 Todo API에서 컨트롤러가 서비스, 서비스가 저장소를 쓴다고 하자.
스프링을 안 쓰고 그냥 자바로 만들면 보통 이렇게 시작한다.
class TodoController {
private final TodoService service = new TodoService(); // 직접 생성
}
class TodoService {
private final TodoRepository repo = new TodoRepository(); // 또 직접 생성
}
class TodoRepository {
// DB 접근
}
이 방식의 문제
- 강한 결합
- TodoService가 TodoRepository를 직접 생성하면
- 저장소 구현을 바꾸는 순간(DBRepo → MemoryRepo) 코드 전체가 흔들린다.
- 테스트가 어려움
- TodoService만 테스트하고 싶은데, 내부에서 new TodoRepository()를 해버리면
- 가짜 저장소(mock/fake)를 끼우기 힘들다.
- 중복 생성/관리 난잡
- 여기저기서 new가 발생하면 “객체가 언제 몇 개 만들어졌는지” 추적이 어렵다.
이걸 한 번에 해결하는 철학이 IoC/DI/Bean이다.
2. IoC Container (Inversion of Control)
객체 생성/연결의 제어권이 개발자 코드에서 ‘컨테이너’로 넘어간다.
스프링에서는 이 컨테이너를 보통 ApplicationContext(스프링 컨테이너) 라고 부른다.
컨테이너가 하는 일:
- 객체를 만들고(생성)
- 필요한 객체끼리 연결해주고(주입)
- 생명주기까지 관리한다(시작/종료)
3. Bean
스프링 컨테이너가 만들어서 등록하고 관리하는 객체
- new로 만든 객체: 그냥 객체
- 스프링이 만들어서 관리하는 객체: 빈(Bean)
빈은 기본적으로 싱글톤 스코프가 많다.
즉, 애플리케이션 전체에서 보통 1개 만들어 공유한다.
“싱글톤 = 고유하다”가 아니라
“애플리케이션 실행 동안 같은 인스턴스를 재사용한다”가 더 정확하다.
4. DI (Dependency Injection)
객체가 의존성을 직접 만들지 않고, 외부에서 주입받는다.
스프링에서는 외부가 누구냐?
- 바로 IoC 컨테이너다.
5. DI의 정석: 생성자 주입(기본값처럼 쓰는 이유)
스프링에서 DI 방법은 여러 개가 있지만, 기본적으로 생성자 주입이 쓰인다.
@RestController
class TodoController {
private final TodoService service;
public TodoController(TodoService service) {
this.service = service;
}
}
@Service
class TodoService {
private final TodoRepository repo;
public TodoService(TodoRepository repo) {
this.repo = repo;
}
}
@Repository
class TodoRepository { }
생성자 주입이 기본인 이유
- 필수 의존성 강제: 필요한 게 없으면 생성 자체가 안 됨
- final 사용 가능: 불변 구조 만들기 쉬움
- 테스트 쉬움: 가짜 repo를 constructor로 넣으면 끝
- 설계가 드러남: 클래스가 뭘 필요로 하는지 코드만 봐도 보임
6) “IoC/Bean/DI”를 흐름으로 묶으면
스프링 부트 앱이 뜰 때 내부에서 대략 이런 일이 벌어진다.
- 스프링 컨테이너(ApplicationContext) 생성
- 컴포넌트 스캔으로 클래스 탐색: @RestController, @Service, @Repository 같은 것들
- 해당 클래스의 인스턴스를 만들고 → Bean으로 등록
- 생성자 파라미터를 보고 의존 빈을 찾아 → DI로 주입
- 기동 완료
컨테이너가(IoC) 객체를 만들고(Bean) 연결한다(DI)
7. @Autowired는 어디에 끼는가(짧게만)
DI를 표현하는 방법 중 하나가 @Autowired다.
그런데 생성자 주입을 쓰면 스프링이 보통 알아서 주입해줘서 @Autowired가 생략되는 경우가 많다.
@Service
class TodoService {
private final TodoRepository repo;
// 생성자 1개면 보통 @Autowired 없어도 주입됨
public TodoService(TodoRepository repo) {
this.repo = repo;
}
}
필드 주입처럼 쓰면 이렇게 되지만, 실제로는 지양하는 편이다.
@Autowired
private TodoRepository repo;
8. “new vs DI”를 한 문장으로 비교
- new 지옥: 객체가 객체를 직접 만들고 직접 연결한다 → 결합이 강함
- DI 구조: 객체는 “필요한 것”만 선언하고, 연결은 컨테이너가 한다 → 결합이 약함
'백엔드' 카테고리의 다른 글
| [스프링 로드맵] B-3. DI/Bean 실전 - @Autowired, @Qualifier, Profile (0) | 2026.01.19 |
|---|---|
| [스프링 로드맵] B-2. DI/Bean 내부 - 빈 등록 방식과 라이프사이클 (1) | 2026.01.19 |
| [스프링 로드맵] A-2. REST 확장 - POST/PUT/DELETE와 ResponseEntity (0) | 2026.01.19 |
| [스프링 로드맵] A-1. REST 입문 - JSON이 응답되는 이유 (0) | 2026.01.19 |
| [스프링 로드맵] 0편. 전체 그림 – REST부터 JPA까지 한 번에 보기 (0) | 2026.01.19 |