IoC (Inversion of Control, 제어의 역전)
일반적인 프로그램은 개발자가 객체를 생성하고, 실행하고, 의존성을 맺어주는 등 모든 흐름을 직접 제어한다.
하지만 IoC가 적용되면 이러한 제어권이 개발자가 아닌 프레임워크(스프링)로 넘어간다.
직접 요리 재료를 사고 요리하는 기존 방식에서 밀키트 서비스가 재료를 다 손질해 주고 나는 조리만 하는 방식이다.
스프링에서는 객체 관리와 유연성 향상을 해 준다.
- 객체 관리: 스프링 컨테이너(IoC 컨테이너)가 객체(Bean)의 생명주기(생성 -> 의존성 설정 -> 소멸)를 대신 관리한다.
- 유연성 향상: 개발자는 비즈니스 로직에만 집중할 수 있게 된다.
스프링 컨테이너

IoC는 스프링 컨테이너에서 일어난다.
- BeanFactory: 스프링 설정 파일에 등록된 Bean을 생성하고 관리하는 가장 기본적인 컨테이너이다.
'Lazy Loading'(실제 사용할 때 객체 생성) 방식을 사용한다. - ApplicationContext: BeanFactory를 상속받은 더 강력한 컨테이너이다.
Bean 관리뿐만 아니라 국제화(i18n), 이벤트 발행, 리소스 로딩 등 부가 기능을 제공한다.
대부분의 실무에서는 이를 사용하며, 'Eager Loading'(구동 시점에 객체 생성) 방식을 사용한다.
DI (Dependency Injection, 의존성 주입)
DI는 IoC를 구현하는 구체적인 방법 중 하나다.
객체가 스스로 필요한 다른 객체를 찾는 것이 아니라, 외부(스프링 컨테이너)에서 주입해 주는 방식이다.
스프링에서는 결합도를 낮추고 코드 재사용 및 테스트 용이하게 할 수 있다.
- 결합도 낮춤: 객체 간의 결합도를 낮춰 한 클래스를 수정해도 다른 클래스에 미치는 영향이 줄어듭니다.
- 코드 재사용 및 테스트 용이: 가짜 객체(Mock)를 주입하기 쉬워져 단위 테스트가 용이해집니다.
의존성 주입 방식
DI에는 세 가지 방법이 있지만, 왜 생성자 주입이 권장되는지 논리적으로 설명할 수 있어야 한다.
- 생성자 주입 (Constructor Injection): 현재 가장 권장되는 방식.
- 수정자 주입 (Setter Injection): 선택적인 의존성이 필요할 때 사용.
- 필드 주입 (Field Injection): @Autowired를 변수에 바로 붙이는 방식. 코드는 짧지만 외부에서 변경이 불가능해 테스트가 어렵고 프레임워크에 너무 의존적이라 권장되지 않음.
생성자 주입을 써야 하는 이유:
- 불변성: 객체 생성 시점에 딱 1번만 호출되므로 의존 관계가 변하지 않게 보장한다.
- 필드/수정자 주입의 문제: 객체를 생성한 후에 언제든지 setter를 통해 의존성을 바꿀 수 있으므로, 실행 중에 의존성을 바꿔버리면 프로그램이 예상치 못한 방향으로 고장날 수 있다.
- 생성자 주입의 해결: final 키워드를 사용하면, 반드시 생성자에서 값이 할당되어야하며, 절대 수정할 수 없다.
- 결과: 의존성이 빠지거나 바뀔 걱정 없이 안심하고 객체를 사용할 수 있다.
- 누락 방지: 컴파일 시점에 의존 관계가 누락되었음을 바로 알 수 있다.
- 순환 참조 방지: 두 객체가 서로를 참조하는 '순환 참조' 문제를 애플리케이션 구동 시점에 바로 잡아낼 수 있다.
- 필드/수정자 주입: 일단 객체 생성은 성공하지만, 실제 그 기능을 호출하는 시점에 서로 끝없이 호출하다가 서버가 죽어버린다.
- 생성자 주입: 애플리케이션을 켜자마자 스프링이 A를 만드려니 B가 필요하고, B를 만드려니 A가 필요하기 때문에 만들지 못해버린다. ( BeanCurrentlyInCreationException )
- 결과: 잘못된 설계를 서버가 돌아가기 전에 즉시 알 수 있다.
언제 사용하는가?
기존 방식 (제어권이 개발자에게 있음)
public class UserService {
private UserRepository userRepository = new MariaDBUserRepository(); // 직접 생성
}
위 코드에서는 UserService가 특정 DB 구현체에 강하게 결합되어 있어, DB를 바꾸려면 코드를 직접 수정해야 한다.
DI 방식 (제어권이 스프링에게 있음)
@Service
public class UserService {
private final UserRepository userRepository;
// 생성자 주입: 스프링이 알아서 UserRepository 구현체를 넣어줌
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
이제 UserService는 어떤 DB가 들어오는지 몰라도 된다.
스프링 설정에 따라 MariaDB든 PostgreSQL이든 자유롭게 주입할 수 있다.
Lombok 사용 시
@Service
@RequiredArgsConstructor // Lombok: final 필드에 대한 생성자를 자동으로 생성
public class MemberService {
private final MemberRepository memberRepository; // final 키워드로 불변성 보장
// 생성자 주입이 자동으로 일어남
}
DI를 사용하면?
DI를 통해 객체를 직접 생성하지 않고 인터페이스를 주입받게 되면, 우리는 아주 강력한 무기를 얻게 된다.
그것이 바로 PSA(Portable Service Abstraction)이다.
PSA (Portable Service Abstraction)
- 비유: '멀티탭'이나 '범용 어댑터'와 같다. 우리가 가전제품(비즈니스 로직)을 쓸 때 내부 전기 배선이 어떻게 되어 있는지 몰라도 코드만 꽂으면 작동하듯, 스프링도 내부 기술(JPA, JDBC 등)이 바뀌어도 우리는 똑같은 어노테이션과 인터페이스를 쓰게 해 준다.
- 대표적인 예시: @Transactional
- 내부적으로 JDBC를 쓰든, JPA를 쓰든, 혹은 마이바티스(MyBatis)를 쓰든 개발자는 오직 @Transactional 하나만 붙이면 트랜잭션 관리가 된다.
- 스프링이 각 기술에 맞는 트랜잭션 관리 방식을 내부적으로 추상화해서 제공하기 때문이다.
@Service
public class OrderService {
@Transactional // 이 어노테이션 하나로 JDBC, JPA 등 내부 기술에 상관없이 트랜잭션 관리!
public void processOrder() {
// 주문 로직...
}
}
- 왜 쓰는가?
- 특정 기술(예: 특정 DB 라이브러리)에 코드가 종속되지 않는다. (결합도 낮춤)
- 기술을 변경해야 할 때 비즈니스 로직을 수정할 필요가 없어 유지보수가 매우 편리하다.
무엇을 관리하고 어떻게 사용하나요?
지금까지 어떻게 사용했는지를 알았봤다면, 무엇을 관리하고 어떻게 사용하지, 어떤 구조로 사용되는지를 간단하게 알아보면,
앞서 Bean은 스프링이 직접 관리하는 자바 객체로, 우리가 직접 new를 사용해서 만드는 것이 아닌 스프링이 대신 만들어서 컨테이너 안에 담아두는 객체이다.
이 빈을 등록하는 법은 @Component 또는 하위 어노테이션(@Controller, @Service, @Repository 등)으로 자동 등록하거나, @Bean이라는 어노테이션을 활용한다.
@Configuration //: 개발자가 직접 코드를 수동으로 빈을 등록하고 관리할 때 사용.
public class AppConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate(); // 외부 객체를 수동으로 빈 등록
}
}
결국, 빈(Bean)을 등록하고 의존성을 주입(DI)하는 모든 과정을 스프링 컨테이너가 전담하기 때문에 우리는 이를 '제어의 역전(IoC)'이라고 부른다.
면접에서 물어보는 이유
면접관이 이 질문을 던지는 이유는 단순히 용어의 정의를 아는지 확인하려는 것이 아니다.
- 객체지향 설계 원칙(SOLID): 특히 DIP(의존 역전 원칙)와 전략 패턴에 대한 이해도를 파악하기 위함이다.
- 프레임워크의 본질: "왜 굳이 스프링을 쓰는가?"에 대한 답을 알고 있는지 확인한다.
- 유지보수 능력: 결합도가 낮은 코드가 왜 유지보수에 유리한지, 테스트 코드를 짜본 경험이 있는지를 간접적으로 묻는 것이다.
면접 답변식 요약
IoC(제어의 역전)는 객체의 생명주기나 흐름에 대한 제어권이 개발자가 아닌 프레임워크에 있는 것을 의미합니다.
이를 통해 개발자는 객체 관리 부담을 덜고 핵심 로직에 집중할 수 있습니다.
DI(의존성 주입)는 이러한 IoC를 구현하는 기법으로, 객체가 사용할 의존 객체를 직접 생성하지 않고 외부(컨테이너)로부터 주입받는 방식입니다.
스프링에서 이들은 객체 간의 결합도를 낮추고 유연성을 높이는 역할을 합니다.
예를 들어, 인터페이스를 기반으로 DI를 구현하면 구현체가 바뀌더라도 사용하는 쪽의 코드를 수정할 필요가 없어 유지보수와 단위 테스트가 매우 용이해집니다.
'Daily Dev Q&A 정리 템플릿' 카테고리의 다른 글
| 25.12.26 Spring Bean은 무엇인가요? (0) | 2025.12.26 |
|---|---|
| 25.12.24 스프링 부트은 스프링과 다른 건가요? (0) | 2025.12.24 |
| 25.12.22 스프링 프레임워크이란? (0) | 2025.12.22 |
| 25.12.19 트랜잭션에 대해 알아보기 (1) | 2025.12.19 |
| 25.12.17 정규화에 대해서 설명해주세요. (0) | 2025.12.17 |