스프링 의존성 주입(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라고 한다.
@Component 어노테이션을 사용하여 Spring Bean에 등록이 가능하고,
import org.springframework.stereotype.Component;
import java.util.Base64;
@Component("base74Encoder") //base74Encoder라는 이름으로 bean 등록
public class Base64Encoder implements IEncoder {
public String encode(String message){
return Base64.getEncoder().encodeToString(message.getBytes());
}
}
등록된 bean을 가져올 수 있도록 context 설정한다.
@Component
public class ApplicationContextProvider implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
// context를 return
public static ApplicationContext getContext() {
return context;
}
}
사용할 때는 context를 사용하여 가져올 수 있다.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@SpringBootApplication
public class SpringIocApplication {
public static void main(String[] args) {
SpringApplication.run(SpringIocApplication.class, args);
ApplicationContext context = ApplicationContextProvider.getContext();
//Base64Encoder base64Encoder = context.getBean(Base64Encoder.class);
//UrlEncoder urlEncoder = context.getBean(UrlEncoder.class);
Encoder encoder = context.getBean("urlEncode", Encoder.class);
String url = "www.naver.com/books/it?page=10&size=20&name=spring-boot";
String result = encoder.encode(url);
System.out.println(result);
}
}
@Configuration
class AppConfig{
@Bean("base64Encode")
public Encoder encoder(Base64Encoder base64Encoder){
return new Encoder(base64Encoder);
}
@Bean("urlEncode")
public Encoder encoder(UrlEncoder urlEncoder){
return new Encoder(urlEncoder);
}
}
여러 개를 등록하고 싶을 때는 @Configuration 어노테이션을 사용하여 설정하고, @Bean 어노테이션에 이름을 지정하여 bean에 등록할 수 있다.