Back-end/Spring & JPA

[Spring] DI & 스프링 컨테이너(+ @Autowired)

Uykm 2023. 11. 2. 18:50

▼ Why ? What ?

"빈(Bean)" 을 다루기 위해선 "의존성 주입(DI)" 의 개념을 이해하는 것은 필수적이다. 최근에 GDSC - Web 커리큘럼에서 스프링 시큐리티 환경 설정 클래스를 다룰 때 빈(Bean)을 수동적으로 생성하는 코드를 작성해봤다. 이렇게 생성한 빈(Bean)을 사용하는 과정을 추가적으로 공부해봤는데, 의존관계가 있는 클래스 타입의 빈(Bean)을 "스프링 컨테이너"에서 탐색한 후에 찾은 빈(Bean), 즉 객체를 외부에서 주입해주는 과정이 필요하다. 따라서, DI에 대한 개념을 다잡아보고 스프링 컨테이너에 대해서 공부한 후에 정리하였다.


 DI (Dependency Injection)

 

DI ?

 

  • "의존관계 주입 (DI)" 은 의존관계를 "스프링 컨테이너", 즉 외부에서 객체 간의 관계(의존성)를 결정해주는 것.
      객체를 직접 생성하는 것이 아니라 외부에서 생성 후 주입시켜 주는 방식을 말한다 !
    ( "의존성"객체를 생성하거나 사용함에 있어 의존관계가 있는 경우를 말하고, 의존 대상인 B가 변경되었을 때 그 영향이 A에도 미치게 되면 A는 B와 의존관계가 있다고 할 수 있다. )

 


 

DI가 필요한 이유 ?

 

  • 아래의 코드를 보면 CakeBaker 객체와 CheeseCakeRecipe 객체에 의존관계가 있음을 알 수 있다.
    ➜ 두 클래스(CakeBaker, CheeseCakeRecipe)가 강하게 결합되어 있는 문제  때문에 레시피(ChocolateCakeRecipe 클래스)가 바뀔 때마다 CakeBaker 클래스의 생성자를 매번 변경해줘야 해서 유연성이 매우 떨어진다고 볼 수 있다 !
    ➜ 즉, 객체들 간이 아니라 클래스 간에 관계가 맺어진다고 볼 수 있고, 이는 객체 지향 5원칙(SOLID) 중 "구체화(Implementation; 구현 클래스)에 의존해선 안되고 추상화(Abstraction; 인터페이스)에 의존해야 한다." 라는 DIP 원칙에 위반하는 것이 된다.
    이러한 문제를 해결하기 위해 "의존관계 주입(DI)" 가 필요한 것이다.
    즉, DI를 통해 객체 간의 의존관계를 동적으로 주입하여 유연성을 확보하고 결합도를 낮추는 것이다 !
public class CakeBaker{
	
	private CheeseCakeRecipe cheeseCakeRecipe;
	
	public CakeBaker() {
		this.cheeseCakeRecipe = new cheeseCakeRecipe();
	}
	
}

 


 

 

의존관계 주입 (Dependency Injection) 예시

 

  • CakeRecipe 인터페이스를 구현
public interface CakeRecipe { ... }
public class CheeseCakeRecipe implements CakeRecipe { ... }

 

  • CakeBaker 클래스의 생성자에서 외부로부터 CakeRecipe 객체를 주입(Injection) 받도록 수정
public class CakeBaker {
    
    /*
    private CheeseCakeRecipe cheeseCakeRecipe;
	
	public CakeBaker() {
		this.cheeseCakeRecipe = new cheeseCakeRecipe();
	}
    */
    
    private CakeRecipe cakeRecipe;
    
    public CakeBaker(CakeRecipe cakeRecipe) {
        this.cakeRecipe = cakeRecipe;
    }
    
}

 

  • "스프링 컨테이너" 에서 애플리케이션 실행 시점에 필요한 객체, 즉 빈(Bean)을 생성하여 CakeBaker 클래스에 주입해준다 !
// Bean 생성
CakeRecipe cheeseCakeRecipe = new cheesCakeRecipe();
// DI 주입
CakeBaker cakeBaker = new CakeBaker(cheeseCakeRecipe);

 


 스프링 컨테이너(Spring Container)

 

스프링 컨테이너 ?

 

  • 스프링 프레임워크의 핵심 컴포넌트 "스프링 컨테이너" 는 자바 객체, 즉 빈(Bean)의 생명 주기를 관리하며, 생성된 자바 객체들에게 추가적인 기능을 제공한다.

  • 스프링 컨테이너의 기능
    ➜ 빈(Bean)의 인스턴스화, 구성, 전체 생명 주기 및 제거까지 관리한다.

 


 

스프링에서의 의존관계 주입(DI) - Constructor & Field & Setter Injection

 

  • 스프링 컨테이너가 DI를 담당하고, "빈(Bean)" 에 의존성 주입을 하는 방법은 총 3가지가 있다.
    스프링(Spring)에선 "생성자 주입 (Contructor Injection)" 을 지향하자 !


 

1. Constructor Injection - 생성자 주입

: 스프링(Spring)을 포함한 DI 프레임워크의 대부분이 이 "Constructor Injection" 을 권장하고 있다.
➜ 아래의 코드처럼 '@Autowired' 애너테이션을 사용하는 것이  생성자를 통해 의존관계가 있는 객체(Bean)를 주입하는 방식이다.

  • @Autowired 애너테이션을 붙이면, 해당 생성자(Injection)가 자동으로 호출되고 의존 객체 'injectionService' 의 클래스 타입에 해당하는 빈(Bean)을 찾아서 주입해준다.
    ( 단, InjectionService 클래스가 빈(Bean)으로 등록되어 있어야 한다 ! )
public class Injection {
 
    private InjectionService injectionService;
    
    // 생성자 DI
    @Autowired // Spring4.3부터는 @Autowired 생략 가능
    public Injection(InjectionService injectionService) {
    	this.injectionService = injectionService;
    }
}

 

 

  • @Autowired는 해당 클래스 내에 생성자가 하나만 있을 땐 생략도 가능하다 !
    ➜ 게다가 '@RequiredArgsConstructor' 애너테이션을 함께 사용하게 되면 생성자를 생성하는 코드 없이 "생성자 주입" 이 가능하다 !

    Why ?
    ➜ 롬복(Lombok)에서 제공하는 @RequiredArgsConstructor 애너테이션은 초기화 되지 않은 final 필드@NonNull 애너테이션이 붙은 필드에 대해 생성자를 자동으로 생성해주기 때문이다.
    ( final선언해준 동시에 초기화 시켜줘야 하는데,  꼭 생성자를 통해서만 초기화가 가능하다 ! )
@RequestMapping("/question")
@RequiredArgsConstructor
@Controller
public class QuestionController {

    private final FirstQuestionService firstQuestionService;
    private final SecondQuestionService SecondQuestionService;
    
    /* 이렇게 하나의 생성자만 생성되기 때문에 @Autowired가 생략된 것으로 간주하여
       생성자 주입이 가능한 것이다!
    public QuestionController(FirstQuestionService firstQuestionService, 
                                     SecondQuestionService SecondQuestionService) {
        this.firstQuestionService = firstQuestionService;
        this.secondQuestionService = secondQuestionService;
    }
    */
    
    ...
    
}

🔻 이처럼 final 키워드를 사용해서 값이 한 번 할당되면 변경할 수 없기에 자동적으로 "객체의 불변성(Immutability)" 이 보장되는 장점이 있다 !

➜ 초기에 값이 할당되기 때문에 NPE(Null Pointer Exception)도 절대 발생하지 않는다 !

 

2.  Field Injection - 필드 주입

  • 다음과 같이 빈(Bean)으로 등록된 객체를 주입하고자 하는 클래스에 간단하게 필드로 선언해준 후에 @Autowired 애너테이션만 달아주면 스프링에서 의존성을 주입해준다
    ➜ 단, 이렇게 하면 DI가 생성자 이후에 호출되므로, "생성자 주입" 처럼 필드를 final로 선언할 수 없다 !
    "객체의 불변성" 보장 X
public class Injection {
    @Autowired
    private InjectionService injectionService;
}

 

3. Setter Injection - 수정자 주입

  • Setter 메서드에 @Autowired 애너테이션을 사용해서 주입하는 방식이다.
    이 또한 "생성자 주입" 이 아니기 때문에, 필드에 final 키워드를 사용 X
    따라서 "객체의 불변성" 도 보장되지 않으며 NPE도 발생할 수 있다.
public class Injection {
	
    private InjectionService injectionService;
    
    @Autowired
    public void setInjectionService( InjectionService injectionService) {
    	this.injectionService = injectionService;
    }
}

🔻 코드에서 보이다싶이 수정자 주입 방식을 이용하는 경우엔 Setter 메서드를 public으로 선언해두어야 하기 때문에 안정성이 떨어진다고 볼 수 있다. !