본문 바로가기
Java/Spring

[Spring] 의존관계 주입(Dependency Injection), 의존성 주입, DI

by 전재경 2022. 12. 4.

DI를 알기 전 의존관계에 부터 말하자면

의존관계

의존 대상 B가 변하면, 그것이 A에 영향을 미칠 때 A는 B와 의존관계라 한다.

쉽게 말해 B가 변경되었을 때 그 영향이 A에 미치는 관계

 

피자 가게의 요리사는 피자 레시피에 의존한다. 만약 피자 레시피가 변경된다면, 요리사는 피자를 새로운 방법으로 만들게 된다. 레시피의 변화가 요리사에 미쳤기 때문에 요리사는 레시피에 의존한다라고 할 수 있다.
public class PizzaChef{
	
	private PizzaRecipe pizzaRecipe;
	
	public PizzaChef() {
		this.pizzaRecipe = new PizzaRecipe();
	}
	
}

PizzaChef 객체는 PizzaRecipe 객체에 의존 관계

 

이러한 구조는 문제점들이 있다.

 

두 클래스의 결합성이 높다

PizzaChef 클래스는 PizzaRecipe 클래스와 강하게 결합되어 있다는 문제점을 가지고 있다. 만약 PizzaChef가 새로운 레시피인 CheezePizzaRecipe 클래스를 이용해야 한다면 PizzaChef 클래스의 생성자를 변경해야만 한다. 만약 이후 레시피가 계속해서 바뀐다면 매번 생성자를 바꿔줘야 하는 등, 유연성이 떨어지게 된다. 

 

객체들 간의 관계가 아닌 클래스 간의 관계가 맺어진다

객체 지향 5원칙(SOLID) 중 "추상화(인터페이스)에 의존해야지, 구체화(구현 클래스)에 의존하면 안 된다"라는 DIP 원칙이 존재한다. 현재 PizzaChef 클래스는 PizzaRecipe 클래스와 의존 관계가 있다. 즉, PizzaChef는 클래스에 의존하고 있다. 이는 객체 지향 5원칙을 위반하는 것으로 PizzaChef 클래스의 변경이 어려워지게 된다. 

 

이러한 문제점들을 해결할 수 있는 것이 바로 의존관계 주입(이하 DI)이다.

 

 

 

의존관계 주입(DI)

DI는 의존관계를 외부에서 결정(주입)해주는 것

스프링에서는 이러한 DI를 담당하는 DI 컨테이너가 존재한다. DI컨테이너가 객체들 간의 의존 관계를 주입

 

위의 문제점을 DI를 이용해 해결해보자. 우선 다양한 피자 레시피를 추상화하기 위해 PizzaRecipe를 interface로 만들자. 이후 다양한 종류의 피자 레시피는 이 PizzaRecipe 인터페이스를 구현하는 식으로 작성하면 된다.

public interface PizzaRecipe{
	
}

public class CheesePizzaRecipe implements PizzaRecipe{
	
}

이제 PizzaChef 클래스의 생성자에서 외부로부터 피자 레시피를 주입(injection) 받도록 변경하자. 

public class PizzaChef{
	
	private PizzaRecipe pizzaRecipe;
	
	public PizzaChef(PizzaRecipe pizzaRecipe) {
		this.pizzaRecipe = pizzaRecipe;
	}
	
}

이때 스프링의 DI 컨테이너가 애플리케이션 실행 시점에 필요한 객체를 생성하여 PizzaChef 클래스에 주입해주는 역할을 한다. 예를 들어 다음과 같이 동작한다.

// DI 컨테이너에서의 동작

PizzaChef = new PizzaChef(new CheesePizzaRecipe());

// 만약 치즈 피자 레시피에서 베이컨 피자 레시피로 바뀐다면?

PizzaChef = new PizzaChef(new BaconPizzaRecipe());

비유를 들자면 피자가게 사장(스프링 DI 컨테이너)이 피자 셰프에게 특정 피자 레시피를 주입해주는 것이다. 

이렇게 하면 피자 셰프는 피자 레시피가 바뀌더라도 생성자를 변경하지 않아도 된다. 그저 레시피가 바뀐다면 외부에서 바뀐 레시피를 주입해주기만 하면 된다. 

 

이처럼 의존관계를 외부에서 결정하여 주입하는 것이 DI이다.

 

 

DI(의존성 주입) 종류

스프링에서 의존성을 주입하는 방법은 아래와 같이 3가지 방법이 있습니다.

 

필드 주입 (Field Injection)

@Controller
public class TestController {

    @Autowired
    TestService testService;
    
}

가장 코드가 단순하고 저같은 경우 기존에 많이 봐왔고 자주 사용했던 방식인데요, 아래와 같은 단점이 있어 추천되지 않는 방식이라고 합니다.

  • 프레임워크 의존적 : 스프링 DI 컨테이너에서만 동작, 외부에서 수정 불가, 테스트의 어려움
  • final 선언 불가 : 객체 변경 가능 

 

수정자 주입 (Setter Injection)

@Controller
public class TestController {

    private TestService testService;

    @Autowired
    public void setTestService(TestService testService) {
        this.testService = testService;
    }
    
}

수정자 주입 방식은 스프링 3.x대 버전에서 추천되었던 방식으로, 현재는 주입받는 객체가 변경될 가능성이 있을 경우에만 사용되는 방식이라고 합니다.

 

생성자 주입 (Constructor Injection)

@Controller
public class TestController {

    private final TestService testService;

    @Autowired // 생성자가 1개만 있을 경우 생략 가능
    public TestController(TestService testService) {
        this.testService = testService;
    }
    
}

현재 스프링 프레임워크에서 가장 권장되는 방식입니다. 간략히 아래와 같은 장점이 있다고 합니다.

  • 테스트 용이 : 프로엠워크 의존적이지 않아 순수 자바 등 외부 테스트 코드 작성 가능
  • 객체 불변성 확보: final 선언 가능, 유지보수 용이성
  • 순환 참조 에러가 발생할 경우 컴파일 시 판단 가능

생성자 주입 방식도 Lombok의 @RequiredArgsConstructor를 이용하면 코드를 아래와 같이 간결하게 작성할 수 있습니다.

@Controller
@RequiredArgsConstructor
public class TestController {

    private final TestService testService;
    
}

 

댓글