Spring boot/Spring 개념

Spring DI(Dependency Injection)

eunnnn 2023. 4. 24. 06:27

Spring DI(의존성 주입)이란?

A 객체에서 B, C객체를 사용(의존)할 때 A 객체에서 직접 생성 하는 것이 아니라 외부(IOC컨테이너)에서 생성된 B, C객체를 조립(주입)시켜 사용하는 방식이다.
 
의존성 주입은 스프링 컨테이너에 스프링 빈 모두 등록 -> 빈 관계 설정(연관관계 주입)의 순서로 이루어진다. 다만 예외적으로 생성자 주입 방식으로 DI를 수행하는 경우 빈 등록시 생성자가 호출되므로 빈 등록과 동시에 DI가 이루어진다.
 
의존성 주입을 사용하는 이유는 다음과 같다.

  • 유지보수 용이
    만약 A클래스를 서로다른 100개의 클래스에서 사용해야 할 때 주입받지 않는다면 각 클래스는 A클래스를 직접 생성해야 한다. 그런데 만약 A클래스에 변경 사항이 생긴다면 100개의 클래스를 모두 수정해주어야 한다. 반면 주입 받아서 사용하는 경우 A클래스의 변경사항이 생겨도 A클래스의 생성자만 수정하면 되기 때문에 Side Effect가 훨씬 적다.
  • 결합도를 낮출 수 있다
    DI를 사용안할 시 - 강한 결합, A 클래스에서 B 클래스를 직접 생성한다고 했을 때, B 클래스가 아닌 C 클래스로 변경하고 싶은경우에는 A 클래스까지 변경이 발생한다.
    DI 사용 시 - 느슨한 결합. 객체를 주입받게되면 내부에서 직접 생성하는게 아닌 외부에서 생성된 객체를 주입받는 것이다. 이로 인해 런타임시점에 의존관계가 설정되므로 유연한 구조를 가질 수 있다.
  • 객체의 과한 책임에 대한 경고
    주입받는 객체의 개수가 많아질 수록, 필드 주입인 경우 필드가, 생성자 주입인경우 생성자의 파라미터가 늘어날 것이다. 이들이 너무 많아진다면 한 객체에 너무 많은 책임이 있다는 신호다.

 

의존성 주입 방식

의존성 주입 방식은 다음의 4가지가 있다.

  • 필드 주입(Field Injection)
  • 수정자 주입(Setter Based Injection)
  • 생성자 주입(Constructor Based Injection)
  • 일반 메서드 주입

필드 주입
필드 주입은 이름 그대로 클래스에 선언된 필드에 바로 생성된 객체를 주입해주는 방식이다.
스프링에서 제공하는 @Autowired 어노테이션을 주입할 필드위에 명시하여 사용한다. 

  • 코드가 간결해진다.
  • 단, 외부에서 변경이 불가능하여 테스트하기 어려운 단점이 있다.
  • DI 프레임워크가 없으면 아무것도 할 수 없다.
@Controller
public class PetController{
	@Autowired
	private PetService petService;
}

필드 주입을 하게 되면 DI 컨테이너 안에서만 작동하게 된다. 따라서 순수 자바 코드로 테스트하기 어렵다.
또한, final 키워드를 통해 불변 속성이라고 볼 수도 없고, setter로 가변 속성이라고 볼 수도 없는 애매한 상황이 발생하여 잘 사용하지 않는다.
 
수정자 주입
수정자 주입은 setter라고 불리는, 필드의 값을 변경하는 클래스의 수정자를 통해서 의존성을 주입해주는 방식이다.

  • 선택과 변경 가능성이 있는 의존 관계에 사용한다.
  • 자바 빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법이다.
  • set필드명 메서드를 생성하여 의존 관계를 주입한다.
  • @Autowired를 입력하지 않으면 실행이 되지 않는다. @Autowired의 기본 동작은 주입할 대상이 없으면 오류가 발생한다. 주입할 대상이 없어도 동작하게 하려면 @Autowired(required = false)로 설정하면 된다.
@Controller
public class PetController{

    private PetService petService;

    @Autowired
    public void setPetService(PetService petService){
    	this.petService = petService;
    }
}

 
생성자 주입
생성자 주입은 클래스의 생성자를 통해서 의존성을 주입해주는 방식이다. 생성자에 @Autowired를 하면 스프링 컨테이너에 @Component로 등록된 빈에서 생성자에 필요한 빈들을 주입한다.

  • 생성자 호출 시점에 1번만 호출되는 것을 보장한다.
  • 불변과 필수 의존 관계에 사용한다.
  • 생성자가 1개만 존재하는 경우 @Autowired를 생략해도 자동 주입된다.
  • NPE(NullPointerException)를 방지할 수 있다.
  • 주입받을 필드를 final로 선언 가능하다.
@Controller
public class PetController{

    private final PetService petService;

	@Autowired
    public PetController(PetService petService){
    	this.petService = petService;
    }
}

Lombok라이브러리를 통해서 더 간편하게 작성이 가능하다. @RequiredArgsConstructor 로 필드를 포함한 생성자를 포함시켜주고 @Autowired 키워드를 생략해서 더 가독성이 좋은 코드로 사용이 가능하다.

@Controller
@RequiredArgsConstructor
public class PetController{
    private final PetService petService;
}

일반 메서드 주입
이름 그대로 일반 메서드를 통해 주입 받는 것을 말한다.

  • 한 번에 여러 필드를 주입받을 수 있다.
  • 일반적으로 잘 사용하지 않는다.
@Component
public class OrderServiceImpl implements OrderService {

 private MemberRepository memberRepository;
 private DiscountPolicy discountPolicy;
 
@Autowired
public void init(MemberRepository memberRepository, DiscountPolicy 
discountPolicy) {
	this.memberRepository = memberRepository;
	this.discountPolicy = discountPolicy;
 }
}

 

어떤 주입방식이 좋을까?

스프링에서는 생성자 주입 방식을 권장하고 있고, 그 이유는 다음과 같다.
 
객체의 불변성(immutable)
필드, 수정자 주입방식은 예기치 못한 상황으로 빈 객체가 변경될 수 있어 이로 인한 오류(ex.NPE)가 발생할 수 있다. 그러나 생성자 주입 방식의 경우, final을 사용할수 있고 이를 통해 객체 생성 시점에필수족으로 빈 객체 초기화를 수행해야 하기때문에 null이 아님을 보장할 수있고, 초기화 된 객체는 변경 될 수 없다.         
 
순환 참조 문제 방지 가능
순환 참조란 A,B 두 객체가 각각 서로를 필드에 포함하여 참조하고 있는 상태를 말한다.
서로가 서로를 참조하고 있기 때문에 A,B 두 클래스가 맞물려서 서로의 객체를 계속 생성하는 무한반복 상태에 빠지는 것인데, 3가지 방식 모두 순환 참조 문제가 발생하지만 발생 시점이 다릅니다.
필드주입, 수정자주입은 실제 메소드가 호출되었을때 runtime 에러와 함께 수정자 주입 문제가 불거지게 된다. runtime에 에러가 발생하게 되면 미리 예측이 어렵기 때문에 서비스 진행중에 문제를 야기할 수 있다.
그러나 생성자 주입은 스프링 어플리케이션이 구동되는 순간, 즉 컴파일 타임에 에러가 발생한다. 컴파일 타임에 발생하는 에러는 개발자가 쉽게 추적이 가능하기 때문에 서비스 진행전 미리 예방할 수 있다.
 
테스트 용이
테스트가 특정 프레임워크에 의존하는 것은 좋지 않다. 그래서 단위 테스트를 진행할때 순수 자바 코드로 테스트가 가능하는 것이 좋지만, 필드 주입을 사용하는 경우에 순수 자바 코드에서는 DI가 이루어지지않기 때문에 필드가 null 상태가 되어 에러가 발생한다.
 
 

다양한 상황에서의 의존성 주입 

1) 해당 타입의 빈이 없는 경우

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class BookService {

    BookRepository bookRepository;

    @Autowired
    public BookService(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }
}

생성자에 대해 Autowired 어노테이션이 있는 상태이다. BookRepository가 빈으로 등록되어 있지 않다고 가정하면 생성자에서 에러가 발생한다. 이유는 Autowired가 의존성을 주입하기 위해서는 빈이 등록되어 있는 객체중에서 찾는데 BookRepository라는 객체는 빈으로 등록되어 있지 않기 때문에 의존성 주입에 실패해서 에러가 발생한다. (Autowired의 기본값이 true이기 때문이다) 

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class BookService {

    BookRepository bookRepository;
    
    @Autowired(required = false)
    public void setBookRepository(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }
}

위와 같이 쓴다면 BookService의 객체는 빈으로 등록이 되지만 BookRepository는 빈으로 등록되지 않게 된다. (required = false이기 때문이다) 그리고 Autowired를 사용할 수 있는 곳이 필드가 하나 더 있다.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class BookService {

    @Autowired(required = false)
    BookRepository bookRepository;
}

위와 같이 필드에도 사용할 수 있다. setter와 필드에 Autowired를 사용하게 되면 만약 BookRepository가 빈으로 등록되어 있지 않다 해도 BookService는 빈으로 등록이 가능하다. 하지만 생성자에 Autowired를 쓴 상황에서 BookRepository가 빈으로 등록되어 있지 않다면 BookService도 빈으로 등록되지 못하는 경우가 생긴다. 
 
 
2) 해당타입의 빈이 여러 개인 경우
만약 BookRepository라는 인터페이스 아래에 MyBookRepository, YourBookRepository라는 클래스가 존재하고 둘다 빈으로 등록이 되어 있을 때 바로 위의 코드처럼 BookRepository 필드에 Autowired를 적용하면 어떻게 될까? 주입을 해줄 수 없게 된다. 왜냐하면 등록해야 할 빈이 2개이기 때문에 스프링은 어떤걸 해야할지 모르기 때문이다. 

@Repository @Primary
public class MyBookRepository implements BookRepository {
       // test
}

따라서 만약 같은 타입의 빈이 위의 상황처럼 여러개 일 때 내가 만약 MyBookRepository를 빈으로 등록하고 싶다면 @Primary 라는 어노테이션을 추가해주면 MyBookRepository가 빈으로 등록된다. 또 다른 방법으로는 @Qualifier라는 어노테이션을 이용하는 것인데 일반적으로 빈의 이름은 클래스 이름의 스몰케이스이다. 아래의 예시를 보자.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

@Service
public class BookService {

    @Autowired @Qualifier("myBookRepository")
    BookRepository bookRepository;
}

이런식으로 Autowired 옆에 @Qualifier라는 어노테이션을 이용하여 괄호안에는 등록하고자 할 빈의 이름을 적어주면 된다 하지만 @Qualifier보다 @Primary가 더 안전한 방법이기 때문에 @Primary를 더 추천한다. 그리고 마지막으로는 해당타입의 빈을 모두 주입받는 경우도 있다.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class BookService {

    @Autowired
    List<BookRepository> bookRepositoryList;
}

 

위와 같이 List를 이용하면 BookRepository에 해당하는 타입의 빈을 모두 주입받을 수 있다. 
 
 

어떻게 @Autowired 어노테이션만으로 의존성 주입이 가능할까?

public interface BeanPostProcessor

BeanPostProcessor라는 라이프 사이클 인터페이스의 구현체인 AutowiredAnnotationBeanPostProcessor에 의해 의존성 주입이 이루어진다. BeanPostProcessor는 빈의 initializing(초기화) 라이프 사이클 이전, 이후에 필요한 부가 작업을 할 수 있는 라이프 사이클 콜백이다. 그리고 BeanPostProcessor의 구현체인 AutowiredAnnotationBeanPostProcessor가 빈의 초기화 라이프 사이클 이전, 즉 빈이 생성되기 전에 @Autowired가 붙어있으면 해당하는 빈을 찾아서 주입해주는 작업을 하는 것이다.

 
출처