Spring IoC/DI

과녁스·2021년 7월 21일
0

Spring

목록 보기
2/11
post-thumbnail

개요


스프링 의존성 주입(DI)과 제어의 역전(IoC)에 대하여 학습한 내용을 정리해보았습니다.

DI


의존성 주입이란?

객체 생성 시 특정 클래스의 객체를 생성하여 가져오는 경우 특정 클래스의 의존이 된다고 합니다.

스프링에서는 IoC 컨테이너에서 관리하는 Bean 객체를 해당 클래스에서 생성하지 않고 해당 클래스에게 주입해주는 방식을 사용하였습니다.

의존성 주입에는 아래와 같이 3가지의 방식을 사용할 수 있습니다.

필드 주입

필드 주입(Field Injection)은 의존성을 주입하고 싶은 필드에 @Autowired 어노테이션을 붙여주면 의존성이 주입된다.

@RestController
public class PostController {

    @Autowired
    private PostService postService;
    
}

Setter 주입

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을 이용한 생성자 주입을 권장하고 많이 사용합니다.

의존성 주입 사용에 대한 이점

  • 의존성으로부터 격리시켜코드 테스트 용이
  • DI를 통하여 불가능한 상황을 Mock와 같은 기술을 통하여, 안정적인 테스트 가능
  • 코드를 확장 또는 변경할때 영향을 최소화
  • 순환참조(아래 내용 참고)를 막을 수 있음
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 주입방식

필드 주입방식과 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()");
    }
}
  1. 어플리케이션 로딩단계에서는 예외가 발생하지 않습니다.
  2. 클래스 간 순환참조 되는 것이 아닌 메서드가 순환호출 되어야하고 메서드가 순환호출 되는 시점에 예외가 발생합니다.

생성자 주입

먼저 Bean이 생성되는 시점을 알아보면 아래와 같습니다.

스프링 애플리케이션이 로딩되는 시점에 A 클래스가 B 클래스를 의존하고 B 클래스가 C 클래스를 의존한다면 Spring Boot 는 A 클래스에 대한 Bean 을 만들기 위해서 B 클래스의 Bean 을 주입하는데 없으니까, B 클래스의 Bean 을 먼저 만든다. 근데 그 과정에서 또 C 클래스의 Bean 을 주입하는데 없으니까 C 클래스의 Bean 을 먼저 만든다.

  • C -> B -> A 순서로 생성

위 내용을 이해하고 두 클래스 가 서로 의존하는 상황이 발생하면 무한 반복(순환참조)이 발생합니다.

  1. 클래스가 서로 의존성 주입을 통해 순환참조하고 있을 때 발생하는 문제입니다.
  2. 어플리케이션 로딩 시점 에 예외가 발생합니다.

해결 방법

설계상으로 순환참조 문제가 발생할 수 있는 구조를 만들지 않는 것이 가장 좋습니다.

만약 구조상 설계가 어려운 경우에는 @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;
    }
}

그러나 위 방법은 스프링에서 권장하지 않습니다.

IoC


스프링에서는 대부분 개발자가 객체를 생성하여 관리하는 것이 아닌 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에 등록할 수 있다.

출처 및 참고🙏

profile
ㅎㅅㅎ

0개의 댓글