티스토리 뷰
면접 준비할 때 미리 준비 안 해가면 항상 질문 나오는 스프링 IoC와 DI
실제로 프레임워크팀에서 백엔드 개발을 하면서 항상 고려해야 하는 개념이지만
면접에서 물어보면 이게 그렇게 답변하기가 힘들드라
외우는 방식 말고 이해할 수 있게 작성하는 글
✔️ Spring의 IoC와 DI 개념
스프링 프레임워크를 지탱하는 가장 거대한 뿌리, IoC(Inversion of Control)와 DI(Dependency Injection)에 대해 알아본다
이 개념들을 제대로 이해하면 "왜 스프링을 써야 하는가?"에 대한 답을 찾을 수 있을 것이다.
IoC (Inversion of Control, 제어의 역전) : 내가 직접 관리하던걸 프레임워크가 대신 관리해줌
DI (Dependency Injection, 의존성 주입) : 필요한 부품을 내가 만들지 않고 외부에서 넣어줌
일상 비유로 보는 IoC와 DI
상황1) 내가 직접 요리하기 (기존방식)
배가 고픈 당신은 파스타를 만들기로 한다. 마트에 가서 면과 소스를 직접 고르고, 냄비를 꺼내 물을 끓인다. 모든 재료의 선택과 조리 과정의 주도권은 당신(개발자)에게 있다.
상황2) 레스토랑에서 주문하기 (IoC/DI방식)
당신은 레스토랑에 앉아 "파스타 하나 주세요"라고 주문한다. 주방장(Spring Container)이 알아서 좋은 면과 소스를 골라 요리한 뒤 당신의 테이블에 내어준다. 당신은 재료가 어디서 왔는지 신경 쓸 필요 없이, 만들어진 파스타를 먹기만 하면 된다.
✔️ IoC (Inversion of Control, 제어의 역전)
IoC는 말 그대로 프로그램의 제어 흐름을 개발자가 직접 관리하는 것이 아니라, 외부(프레임워크)에 맡기는 것을 의미한다.
- 기존 : 개발자가 New Service()를 호출하여 객체를 직접 생성하고 관리함
- IoC : 객체의 생성부터 생명주기 관리까지 모두 Spring Container가 담당함
핵심 오브젝트 : Bean과 컨테이너
- Bean : 단순한 객체가 아님. 스프링이 직접 생성하고, 의존관계를 설정하며, 주입까지 담당하는 애플리케이션의 최소 단위
- ApplicationContext : '빈 공장(BeanFactory)'의 확장판으로, 스프링에서 IoC를 담당하는 핵심 컨테이너. 어떤 객체를 생성하고 연결할지 결정하는 '지휘자'의 역할을 함
✔️ DI (Dependency Injection, 의존성 주입)
IoC가 '제어권을 넘기는 현상'이라면, DI는 그 현상을 구현하는 실제 기법이다. 클래스 사이의 의존 관계를 빈 설정 정보를 바탕으로 컨테이너가 자동으로 연결해주는 것을 말한다.
그러면 어떻게 활용될까?
가장 권장되는 방식은 생성자 주입(Constructor Injection)이다.
@Service
public class OrderService {
private final DiscountPolicy discountPolicy;
// 생성자를 통해 외부(스프링)에서 의존성을 주입해줌
@Autowired
public OrderService(DiscountPolicy discoutPolicy) {
this.discountPolicy = discountPolicy;
}
}
✔️ getBean()과 IoC 오브젝트
스프링 컨테이너가 관리하는 객체를 사용하고 싶을 때 개발자는 ApplicationContext에서 객체를 꺼내온다.
- getBean() : 컨테이너에서 관리 중인 특정 빈을 이름이나 타입으로 찾아오는 메서드
- 하지만 실제 개발 시에는 @Autowired를 통해 자동으로 주입받으므로, getBean()을 직접 호출하는 경우는 드물다
(→ 안정성을 생각하면 getBean() 방식이 더 좋은 거 아닌가 ???? )
✔️ Spring의 생명주기
스프링의 생명주기는 IoC컨테이너라는 주방에서 재료(Bean)가 준비되고, 요리되어 손님에게 나가고, 나중에 뒷정리되는 전체 과정이라고 이해한다.
개발자가 new로 직접 객체를 만들면 생성과 동시에 끝이지만, 스프링이 관리하면 "태어나서 죽을 때까지" 정해진 절차를 따르게 된다.
크게 [생성 → 설정 → 사용 → 소멸]의 단계를 밟는다.
① 스프링 컨테이너 생성 : ApplicationContext가 만들어진다.
② 스프링 빈 생성 : 설정 정보를 보고 클래스의 인스턴스를 생성한다 (생성자 주입은 이 단계에서 발생)
③ 의존관계 주입 : @Autowired 같은 의존성들을 연결해준다. (수정자/필드 주입)
④ 초기화 콜백 : 빈이 준비되었음을 알리고 필요한 설정을 마친다. (@PostConstruct)
⑤ 사용 : 애플리케이션에서 빈을 가져와 로직을 수행한다.
⑥ 소멸 전 콜백 : 컨테이너가 종료되기 직전, 자원을 반납하는 등 정리 작업을 한다. (@PreDestroy)
⑦ 스프링 종료 : 빈과 컨테이너가 소멸한다.
여기서, 단순히 객체가 생성되는 것과 서비스가 준비되는 것은 다르다
- 초기화(Initialize) : 데이터베이스 커넥션 풀을 미리 연결해 두거나, 외부 서버와 소켓을 열어두는 등 무거운 작업은 객체 생성이 끝난 후 의존관계가 다 맺어진 뒤에 수행해야 안전하다.
- 소멸(Destroy) : 애플리케이션이 꺼질 때 열려있던 파일이나 DB 연결을 안전하게 닫아줘야 데이터 손실을 막을 수 있다.
실무에서는 과거 복잡한 인터페이스를 직접 구현하는 대신, @PostContruct @PreDestroy 어노테이션으로 끝낸다
@Component
public class NetworkClient {
private String url;
public NetworkClient() {
System.out.println("1. 생성자 호출: 아직 URL이 없음");
}
public void setUrl(String url) {
this.url = url;
}
// 초기화 콜백 : 의존관계 주입이 모두 끝난 뒤 호출함
@PostConstruct
public void init() {
System.out.println("2. 초기화: "+url+" 서버에 연결을 시도");
}
// 소멸 전 콜백 : 컨테이너가 종료되기 직전에 호출됨
@PreDestroy
public void close() {
System.out.println("3. 소멸 : 서버 연결을 안전하게 종료");
}
}
이 코드를 통해 알 수 있는 IoC와 DI
1) 왜 @PostConstruct가 IoC의 증거인가?
IoC의 핵심은 "내가 호출하는 게 아니라, 프레임워크가 나를 호출하는 것"이다.
- 일반적인 자바 코드 : 내가 new로 객체를 만들고, 바로 다음 줄에 myObject.init()을 직접 호출해야 한다. 호출 시점을 내가 결정해야 함
- 스프링의 @PostConstruct : 개발자는 "이 메서드는 준비가 끝나면 실행해 줘"라고 표시(Annotation)만 해둔다. 실제 호출을 스프링 컨테이너가 적절한 시점(의존성 주입이 완료된 직후)에 알아서 실행한다.
즉, 실행 순서에 대한 제어권이 스프링에게 넘어갔기 때문에 전형적인 IoC의 모습인 것이다.
2) DI(의존성 주입)과의 관계는?
@PostConstruct가 의미 있는 이유는 바로 DI 때문이다.
- 객체가 막 생성된 시점(new 직후)에는 아직 의존성(@Autowired로 붙인 객체들)이 주입되지 않았을 수 있다.
- 만약 주입되지도 않은 객체를 사용해 초기화 로직을 실행하면 NullPointerException이 발생하겠지?
- 스프링은 DI를 완벽하게 끝마친 직후에 @PostConstruct가 붙은 메서드를 실행해 준다
"필요한 모든 재료(DI)를 다 갖다 줄 테니, 이제 요리(초기화)를 시작해"라고 스프링이 신호를 주는 셈이다.
✔️ IoC와 DI의 장단점
장점
① 결합도 감소 : 객체 간의 결합도가 낮아져 한 클래스를 수정해도 다른 클래스에 미치는 영향이 적다.
② 유연성 및 확장성 : 코드 수정 없이 설정만으로 구현체를 갈아 끼울 수 있다 (ex. DB 변경)
③ 테스트 용이성 : 가짜 객체(Mock)를 주입하기 쉬워 단위 테스트가 수월해진다.
단점
① 복잡도 증가 : 작은 프로젝트에서는 오히려 구조가 복잡해 보일 수 있다.
② 런타임 에러 : 의존성 주입이 잘못된 경우 컴파일 시점이 아닌 실행 시점에 에러가 날 수 있다.
✔️ 결론
IoC와 DI는 결국 "나 대신 누군가 객체를 관리하게 함으로써, 개발자는 비즈니스 로직에만 집중하겠다"는 목적이다.
IoC는 전략(목적)이고, DI는 그 전략을 수행하는 방법(수단)이다.
✔️ Ps. @Autowired 방식과 getBean()의 차이
1. 실패시점의 차이 (Fail-Fast 원칙)
- @Autowired (사전주입) : 애플리케이션이 서버가 뜰 때(기동 시점) 모든 부품을 조립해본다. 이 때 부품이 없으면 서버가 아예 안뜬다.
- getBean() (지연조회) : 서버는 잘 뜨지만, 사용자가 특정 기능을 클릭해서 실제 로직이 돌아가는 시점(런타임)에 갑자기 에러가 나며 서비스가 멈춘다.
→ 💡결론 : 새벽 3시에 사용자가 결제 버튼을 눌렀을 때 서버가 터지는 것보다, 개발자가 배포하는 순간 "부품이 없어서 배포 안됨"이라고 알려주는 것이 훨씬 안전한 설계이지 않을까? 이를 Fail-Fast(빨리 실패하기) 전략이라고 한다.
2. 객체지향 설계와 침투적 코드
- 침투적 코드 : getBean()을 쓰려면 코드 안에 ApplicationContext라는 스프링의 핵심 클래스가 직접 등장해야 한다. 내 비즈니스 로직이 스프링이라는 프레임워크에 너무 깊게 의존하게 됨
- 비침투적 코드 (POJO) : @Autowired나 생성자 주입을 쓰면, 내 코드는 스프링 없어도 그냥 자바 객체로 동작할 수 있다. 스프링은 단지 밖에서 부품을 넣어줄 뿐이니까
3. 의존성 시각화와 누락 방지
- @Autowired를 사용하면 클래스 상단이나 생성자만 봐도 "아, 이 클래스는 A와 B가 있어야 돌아가는구나"를 한 눈에 알 수 있다.
- getBean()을 코드 곳곳에 숨겨두면, 어떤 의존성이 필요한지 파악하기 어렵고, 실수로 빈 설정을 누락할 확률이 높아진다.
→ 그냥 딱봐도 @Autowired방식이 더 안정적인 것 같은데 ,, 진짜 속도 하나때문에 getBean() 방식을 선택한건진 의문이다
쫌 더 알아봤더니 내가 "프레임워크" 개발을 하는 특수한 상황이라 getBean()을 사용했던걸지도
프레임워크 팀이 getBean()을 선택한 합리적인 이유
1. 선택적 의존성 (Optional Dependency)
프레임워크는 수많은 기능을 제공하지만, 각 업무팀은 그 중 일부만 사용할 수 있다.
- @Autowired : 서비스가 뜰 때 모든 빈을 조립하려 하므로, 업무팀이 특정 기능을 안써서 관련 라이브러리를 빼버리면 서버 기동시 에러가 난다.
- getBean() : 실제 그 기능을 호출할 때만 빈을 찾는다. 업무팀이 해당 기능을 안쓴다면, 관련 빈이 컨테이너에 없어도 서버는 아무 문제 없이 잘 뜬다.
2. 순환 참조(Circular Dependency) 예방
대규모 프레임워크는 기능이 워낙 방대해서 A 기능이 B를 참조하고, B가 다시 A를 참조하는 '순환 참조' 구조가 발생하기 쉽다.
- @Autowired는 서버가 뜰 때 조립을 시도하므로 순환 참조가 있으면 기동 자체가 안된다.
- getBean()은 호출 시점에 빈을 찾으므로, 조립 시점의 순환 참조 문제를 회피할 수 있어 거대 프레임워크의 복잡한 구조를 관리하기에 유리하다.
3. 동적 빈 결정 (Dynamic Strategy)
프레임워크는 상황에 따라 실행 시점에 어떤 구현체를 쓸지 결정해야 할 때가 많다.
예를 들어, 설정 파일 값에 따라 A_SESSION_WAS를 쓸지, B_SESSION_REDIS를 쓸지 분기해야 한다면, 코드가 실행되는 도중에 getBean(beanName)을 호출하는 방식이 훨씬 직관적이고 강력하다.
'Programming > BackEnd' 카테고리의 다른 글
| [BE] 주니어 백엔드 개발자가 반드시 알아야 할 실무 지식 Ch03 (0) | 2025.12.12 |
|---|---|
| [BE] 주니어 백엔드 개발자가 반드시 알아야 할 실무 지식 Ch01~02 (0) | 2025.11.14 |
| [Back] 토큰 기반 인증 시스템 JWT의 특징 + SpringBoot 적용 (2) | 2022.06.15 |
- Total
- Today
- Yesterday
- 그래프탐색
- BOJ
- 이펙티브자바
- DevOps
- 완전탐색
- dfs
- cicd
- EffectiveJava
- docker-compose
- 완탐
- springboot
- 아이템59
- Java
- 조합
- IMAGE
- 순열
- subset
- Container
- 백준
- 운영체제
- docker
- Retrofit2
- BFS
- 알고리즘
- bruteforce
- 토큰기반인증
- dp
- OS
- 아이템61
- 아이템60
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
