SpringEasy#66
Spring의 IoC(제어의 역전)와 DI(의존성 주입)란 무엇인가요?
#Spring#IoC#DI#핵심개념
힌트
객체 생성과 의존 관계 관리를 개발자가 아닌 프레임워크가 담당합니다.
정답 및 해설
Spring의 IoC(제어의 역전)와 DI(의존성 주입)란 무엇인가요?
IoC(Inversion of Control, 제어의 역전)와 DI(Dependency Injection, 의존성 주입)는 Spring 프레임워크의 핵심 원칙입니다. IoC는 객체의 생성과 생명주기 관리를 개발자 코드가 아닌 Spring 컨테이너가 담당한다는 설계 철학이며, DI는 IoC를 구현하는 구체적인 패턴입니다. 이를 통해 결합도를 낮추고 테스트 가능성과 유연성을 높일 수 있습니다.
IoC (Inversion of Control)
전통적인 방식 vs IoC
// 전통적인 방식: 개발자가 의존성을 직접 생성하고 관리
public class OrderService {
// 개발자가 직접 의존 객체를 생성 (강한 결합)
private final EmailService emailService = new EmailService();
private final OrderRepository repo = new JdbcOrderRepository();
public void placeOrder(Order order) {
repo.save(order);
emailService.sendConfirmation(order);
}
}
// 문제점:
// 1. OrderService가 EmailService와 JdbcOrderRepository에 강하게 결합
// 2. 테스트 시 실제 DB, 실제 이메일 서버 필요
// 3. 구현체 변경 시 OrderService 코드 수정 필요
// IoC 방식: 컨테이너가 의존 객체를 생성하고 주입
@Service
public class OrderService {
// 인터페이스에만 의존 (약한 결합)
private final EmailService emailService;
private final OrderRepository repo;
// Spring 컨테이너가 적절한 구현체를 주입
public OrderService(EmailService emailService, OrderRepository repo) {
this.emailService = emailService;
this.repo = repo;
}
}
// 장점:
// 1. 인터페이스에만 의존 → 구현체 변경 시 OrderService 코드 불변
// 2. 테스트 시 Mock 객체 주입 가능
// 3. 객체 생명주기를 Spring이 관리
Spring 컨테이너 (ApplicationContext)
// Spring 컨테이너의 역할
// 1. 빈(Bean) 정의를 읽어들임 (@Component, @Configuration 등)
// 2. 빈 인스턴스 생성
// 3. 의존성 주입 (DI 수행)
// 4. 빈 생명주기 관리 (초기화, 소멸)
// XML 기반 설정 (레거시)
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
// Java Config 기반 설정
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
// Spring Boot: 자동으로 ApplicationContext 생성
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args); // 내부에서 ApplicationContext 생성
}
}
DI (Dependency Injection)
Spring은 세 가지 방법으로 의존성을 주입합니다.
1. 생성자 주입 (권장)
@Service
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
// @Autowired 생략 가능 (생성자가 하나뿐일 때 Spring 4.3+)
@Autowired
public UserService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
// Lombok으로 더욱 간결하게
// @RequiredArgsConstructor + private final 필드
}
// Lombok 활용
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository; // @NonNull이 없어도 생성자 주입
private final EmailService emailService;
}
생성자 주입의 장점:
// 1. 불변성: final 필드로 선언 가능
@Service
public class PaymentService {
private final PaymentGateway gateway; // 한 번 주입 후 변경 불가
public PaymentService(PaymentGateway gateway) {
this.gateway = gateway;
}
}
// 2. 필수 의존성 강제: 생성자 호출 시 반드시 제공
// new PaymentService() // 컴파일 에러 → 의존성 누락 방지
// 3. 테스트 용이성: new로 직접 생성 가능
@Test
void testPayment() {
PaymentGateway mockGateway = mock(PaymentGateway.class);
PaymentService service = new PaymentService(mockGateway); // Spring 없이 테스트 가능
// ...
}
// 4. 순환 의존성 조기 감지: 애플리케이션 시작 시 즉시 발견
// A → B → A 순환 의존: 시작 시 BeanCurrentlyInCreationException 발생
2. Setter 주입
@Service
public class NotificationService {
private MessageSender messageSender;
@Autowired
public void setMessageSender(MessageSender messageSender) {
this.messageSender = messageSender;
}
public void notify(String message) {
if (messageSender != null) {
messageSender.send(message);
}
}
}
setter 주입의 특징:
- 선택적 의존성에 적합 (없어도 동작 가능한 경우)
- 주입 후 변경 가능 (유연하지만 불변성 보장 불가)
null체크 필요
3. 필드 주입 (@Autowired)
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository; // 필드 직접 주입
@Autowired
private CacheService cacheService;
}
필드 주입의 단점 (비권장 이유):
// 1. 테스트 어려움: Spring 컨텍스트 없이는 의존성 주입 불가
@Test
void testWithoutSpring() {
ProductService service = new ProductService();
// service.productRepository == null → NullPointerException
// 리플렉션 없이는 주입할 방법 없음
}
// 2. final 필드 불가 → 불변성 보장 불가
// @Autowired
// private final ProductRepository repo; // 컴파일 에러 (final + @Autowired 불가)
// 3. 순환 의존성이 숨겨질 수 있음 (런타임에야 발견)
// 4. DI 컨테이너에 강하게 결합된 설계
@Configuration과 Java Config
@Configuration
public class AppConfig {
// @Bean 메서드로 빈을 수동 등록
@Bean
public DataSource dataSource() {
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
ds.setUsername("user");
ds.setPassword("password");
return ds;
}
@Bean
public UserRepository userRepository(DataSource dataSource) {
// dataSource는 Spring이 자동으로 위의 dataSource() 빈을 주입
return new JdbcUserRepository(dataSource);
}
@Bean
public UserService userService(UserRepository userRepository, EmailService emailService) {
return new UserService(userRepository, emailService);
}
}
의존성 주입의 실전 패턴
인터페이스 기반 설계
// 인터페이스 정의
public interface PaymentGateway {
PaymentResult process(PaymentRequest request);
}
// 구현체 1: 카드 결제
@Component
public class CardPaymentGateway implements PaymentGateway {
@Override
public PaymentResult process(PaymentRequest request) {
// 카드 결제 처리
return PaymentResult.success();
}
}
// 구현체 2: 카카오페이
@Component
public class KakaoPayGateway implements PaymentGateway {
@Override
public PaymentResult process(PaymentRequest request) {
// 카카오페이 처리
return PaymentResult.success();
}
}
// 여러 구현체 중 선택 방법
@Service
public class OrderService {
private final PaymentGateway paymentGateway;
// @Qualifier로 특정 빈 선택
public OrderService(@Qualifier("cardPaymentGateway") PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
}
조건부 빈 등록 (@Profile, @Conditional)
// 환경별 다른 구현체 사용
@Component
@Profile("production")
public class RealEmailService implements EmailService {
// 실제 이메일 발송
}
@Component
@Profile("test")
public class MockEmailService implements EmailService {
// 테스트용 이메일 (실제 발송 안 함)
}
// 테스트에서
@SpringBootTest
@ActiveProfiles("test")
class OrderServiceTest {
@Autowired
private EmailService emailService; // MockEmailService 주입됨
}
IoC 컨테이너의 종류
// BeanFactory: 기본 컨테이너 (지연 로딩)
BeanFactory factory = new XmlBeanFactory(new ClassPathResource("beans.xml"));
// ApplicationContext: BeanFactory 확장 (즉시 로딩 + 추가 기능)
// - 국제화 (MessageSource)
// - 이벤트 발행 (ApplicationEventPublisher)
// - 리소스 로딩
// - AOP 지원
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
// Spring Boot의 WebApplicationContext
// - 웹 요청 관련 빈 포함
// - request, session 스코프 지원
DI와 테스트
// 생성자 주입 덕분에 Spring 없이 단위 테스트 가능
class UserServiceTest {
private UserService userService;
private UserRepository mockRepository;
private EmailService mockEmailService;
@BeforeEach
void setUp() {
mockRepository = mock(UserRepository.class);
mockEmailService = mock(EmailService.class);
// Spring 없이 직접 생성!
userService = new UserService(mockRepository, mockEmailService);
}
@Test
void 회원가입_성공() {
// given
User newUser = new User("홍길동", "hong@example.com");
when(mockRepository.save(any())).thenReturn(newUser);
// when
userService.register(newUser);
// then
verify(mockRepository).save(newUser);
verify(mockEmailService).sendWelcome(newUser);
}
}
IoC/DI 원칙 비교 요약
| 구분 | IoC | DI |
|---|---|---|
| 정의 | 제어 흐름을 프레임워크에 위임하는 설계 원칙 | IoC를 구현하는 구체적인 패턴 |
| 관계 | IoC는 상위 개념 | DI는 IoC의 구현 방법 중 하나 |
| 핵심 역할 | Spring 컨테이너(ApplicationContext)가 생명주기 관리 | 외부에서 의존 객체를 주입 |
| 생성자 주입 | 권장 | 불변성, 필수 의존성 강제, 테스트 용이 |
| setter 주입 | 제한적 권장 | 선택적 의존성에 적합 |
| 필드 주입 | 비권장 | 테스트 어렵고 final 불가 |
| 장점 | 결합도 감소, 유연성 향상 | 모의 객체 사용 용이, 구현체 교체 쉬움 |
| 핵심 인터페이스 | ApplicationContext | @Autowired, @Qualifier |