스프링 의존성 주입(DI)과 제어의 역전(IoC)에 대하여 학습한 내용을 정리해보았습니다.
객체 생성 시 특정 클래스의 객체를 생성하여 가져오는 경우 특정 클래스의 의존이 된다고 합니다.
스프링에서는 IoC 컨테이너에서 관리하는 Bean 객체를 해당 클래스에서 생성하지 않고 해당 클래스에게 주입해주는 방식을 사용하였습니다.
의존성 주입에는 아래와 같이 3가지의 방식을 사용할 수 있습니다.
필드 주입(Field Injection)은 의존성을 주입하고 싶은 필드에 @Autowired 어노테이션을 붙여주면 의존성이 주입된다.
@RestController
public class PostController {
@Autowired
private PostService postService;
}
setter 메서드에 @Autowired 어노테이션을 붙여 의존성을 주입하는 방식입니다.
@RestController
public class PostController {
private PostService postService;
@Autowired
public void setPostService(PostService postService){
this.postService = postService;
}
}
생성자를 사용하여 의존성을 주입하는 방식입니다.
@RestController
public class PostController {
private final PostService postService;
public PostController(PostService postService){
this.postService = postService;
}
}
lombok의 @RequiredArgsConstructor 를 사용하면 final로 선언된 필드를 가지고 생성자를 만들어줍니다. 필드가 추가되거나 삭제되어도 @RequiredArgsConstructor에 의해 생성자 코드를 개발자가 수정할 필요가 없습니다.
@RequiredArgsConstructor
@RestController
public class PostController {
private final PostService postService;
}
최근에는 lombok을 이용한 생성자 주입을 권장하고 많이 사용합니다.
package com.example.test;
public class Main {
public static void main(String[] args) {
String testUrl = "google.co.kr/spring";
// Encoder 객체에 객체를 주입하여 다른 동작이 일어나도록 설정
Encoder encoder = new Encoder(new Base64Encoder());
String result = encoder.encode(url);
System.out.println(result);
}
}
여러 Bean들의 서로 연결되어 있음을 의미합니다.
BeanA -> BeanB -> BeanC -> BeanA
여러 Bean들이 순환참조로 인하여 앱 실행이 되지 않는 문제가 발생할 수 있습니다.
특정 클래스에서 IoC 컨테이너에 존재하는 Bean을 주입받기 위해서 필드 주입방식, Setter 주입방식, 생성자 주입방식을 사용합니다.
이 중 생성자 주입방식만 다르게 동작을 하게 됩니다.
필드 주입방식과 Setter 주입방식에서는 A 클래스가 B 클래스를 의존하고, B 클래스가 A 클래스를 의존하는 상황이더라도 애플리케이션 실행과정에서 예외가 발생하지 않는다.
그리고 두 개의 클래스가 순환참조하고 있다고 하더라도 당장에 문제가 발생하지 않는다. 이러한 상황에서 문제가 되는 순간은 A 클래스의 메소드와 B 클래스의 메소드가 서로 순환참조하고 있는 상황에서 해당 메소드가 호출 되었을 때이다.
@Slf4j
@Service
public class ServiceA {
@Autowired
private ServiceB serviceB;
public void run() {
serviceB.run();
log.info("Called ServiceA.run()");
}
}
@Slf4j
@Service
public class ServiceB {
@Autowired
private ServiceA serviceA;
public void run() {
serviceA.run();
log.info("Called ServiceB.run()");
}
}
먼저 Bean이 생성되는 시점을 알아보면 아래와 같습니다.
스프링 애플리케이션이 로딩되는 시점에 A 클래스가 B 클래스를 의존하고 B 클래스가 C 클래스를 의존한다면 Spring Boot 는 A 클래스에 대한 Bean 을 만들기 위해서 B 클래스의 Bean 을 주입하는데 없으니까, B 클래스의 Bean 을 먼저 만든다. 근데 그 과정에서 또 C 클래스의 Bean 을 주입하는데 없으니까 C 클래스의 Bean 을 먼저 만든다.
- C -> B -> A 순서로 생성
위 내용을 이해하고 두 클래스 가 서로 의존하는 상황이 발생하면 무한 반복(순환참조)이 발생합니다.
설계상으로 순환참조 문제가 발생할 수 있는 구조를 만들지 않는 것이 가장 좋습니다.
만약 구조상 설계가 어려운 경우에는 @Lazy 어노테이션을 사용하는 방법이 있습니다.
@Service
public class ServiceA {
private final ServiceB serviceB;
@Autowired
public ServiceA(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
@Service
public class ServiceB {
private final ServiceA serviceA;
@Autowired
public ServiceB(@Lazy ServiceA serviceA) {
this.serviceA = serviceA;
}
}
그러나 위 방법은 스프링에서 권장하지 않습니다.
스프링에서는 대부분 개발자가 객체를 생성하여 관리하는 것이 아닌 Spring Container에 맡기게 된다.
개발자에서 프레임워크로 제어의 객체관리의 권한이 넘어 갔음으로 '제어의 역전'이라고 한다.
축약해서 정리해보면 스프링에서 객체를 직접 관리하는 것을 Bean이고, Bean을 관리하는 것이 스프링 컨테이너고 스프링 컨테이너가 제어하는 권한을 가져갔기 때문에 제어의 역전, IoC라고 한다.
IoC를 적용하면 객체가 자신의 라이프사이클(생성, 소멸 등)과 의존 관계에 대한 책임을 IoC 컨테이너에 위임하게 됩니다.
즉, 개발자가 직접 필요한 객체를 생성하고 연결하는 것이 아니라, 스프링이 이 객체들을 관리하고, 필요한 객체를 주입해 줍니다. 이렇게 하면 개발자는 비즈니스 로직 구현에만 집중할 수 있습니다.
IoC는 넓은 개념으로 DI(Dependency Injection, 의존성 주입)를 포함합니다. IoC는 제어를 넘긴다는 큰 개념을 의미하고, DI는 IoC의 일종으로, 외부에서 객체의 의존성을 주입해주는 구체적인 방법입니다. 스프링에서는 IoC를 DI로 구현하여 객체 간의 결합을 느슨하게 합니다.
Spring IoC는 ApplicationContext와 BeanFactory라는 두 가지 주요 인터페이스로 구현됩니다. 이 컨테이너들은 스프링 빈(bean)을 관리하는 역할을 하며, 빈 설정과 의존성 주입을 담당합니다.
@Component 어노테이션을 사용하여 Spring Bean에 등록이 가능하고,
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class PaymentService {
public void processPayment() {
System.out.println("Payment processed.");
}
}
@Service
public class OrderService {
private final PaymentService paymentService;
// 생성자 주입 방식으로 PaymentService를 외부에서 주입받음
@Autowired
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
public void placeOrder() {
System.out.println("Placing order...");
paymentService.processPayment();
}
}
스프링 애플리케이션을 실행하는 Application 클래스를 만들어서 실행해보겠습니다.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(Application.class, args);
// IoC 컨테이너에서 OrderService 빈을 가져와서 사용
OrderService orderService = context.getBean(OrderService.class);
orderService.placeOrder();
}
}
이제 OrderService는 IoC 컨테이너로부터 PaymentService를 주입받기 때문에, OrderService와 PaymentService는 서로 강하게 결합되지 않으며, PaymentService가 변경되더라도 OrderService의 코드를 수정할 필요가 없습니다. 또한, 테스트 시에는 PaymentService 대신 다른 Mock 객체를 주입할 수 있어 테스트도 용이해집니다.
여러 개를 등록하고 싶을 때는 @Configuration 어노테이션을 사용하여 설정하고, @Bean 어노테이션에 이름을 지정하여 bean에 등록할 수 있다.