의존성 주입(Dependency Injection)은 스프링의 핵심 개념 중 하나로 흔히 DI로 줄여 쓰기도 한다.
의존관계 주입이라고 하기도 하고 다양한 번역이 존재하지만 이 글에선 의존성 주입으로 통일하도록 하겠다.
1. 의존성 (Dependency)
의존성 주입에 대해 알기 전에 의존성이 무엇인지 알아봐야 할 필요가 있다.
일단 '의존한다'라는 것은 무엇일까?
흔히 우리가 '철수가 영희에게 의존한다'라는 표현을 하면 철수가 영희에 의해 좌지우지된다고 생각할 수 있다. 즉, 영희의 행동과 상태의 변화가 일어나면 그 영향이 철수에게도 미친다는 것이다.
철수와 영희를 프로그램의 일부로써 생각한다면 영희의 기능 변경이나 추가가 철수에도 영향을 끼친다는 것이다.
의존대상 B가 변하면, 그것이 A에 영향을 미친다.
- 토비의 스프링 3.1
쉽게 말해서 클래스 A가 클래스 B의 메서드를 사용한다면, A가 B를 의존한다고 할 수 있다.
실생활에서의 예시로 설명해 보자.
늦은 새벽까지 스프링 공부를 하던 불쌍한 조 모 군(25)은 배가 고프기 시작했다.
그는 배를 채우기 위해 부엌에 가서 라면을 찾기 시작했다.
다행히 찬장을 열어보니 볶음면과 국물라면이 하나씩 있다.
이 경우에 조 모 군은 라면의 종류에 따라 라면 조리법을 바꿔 적용해야 한다.
국물라면을 볶음면으로, 볶음면을 국물라면으로 먹으면 맛이 없지 않은가!
이런 경우에 "라면 조리법은 라면 종류에 의존한다."라고 말할 수 있다. 이를 코드로 표현해 보면 아래와 같다.
public class RamenRecipe {
private Buldak buldak; // 찬장에서 불닭볶음면 발견
public RamenRecipe() { // 라면 조리법은
buldak = new Buldak(); // 불닭볶음면의 조리법을 사용
}
}
2. 의존성의 추상화
위의 RamenRecipe의 예시를 보면, 단지 Buldak(불닭볶음면)에만 의존하고 있음을 확인할 수 있다.
하지만 조 모 군은 평생 불닭볶음면만 먹고 싶지 않다. 신라면도 먹고, 짜파게티도 먹고 싶다!
조 모 군이 더 다양한 라면을 먹을 수 있게 하려면 인터페이스로 추상화가 필요하다.
public class RamenRecipe {
private Ramen ramen; // 인터페이스로 추상화가 됨
public RamenRecipe() { // 이제 다양한 라면에 대한 레시피를 알 수 있음
ramen = new Buldak(); // 불닭볶음면
//ramen = new Sinramen(); // 신라면
//ramen = new Jjapagetti(); // 짜파게티
}
}
public interface Ramen { // Ramen 인터페이스
newRamen();
...
}
public class Buldak implements Ramen { // 불닭볶음면 클래스
public Buldak newRamen() {
return new Buldak();
}
...
}
이제 조 모 군은 불닭볶음면뿐만 아니라 신라면, 짜파게티와 같은 다양한 라면의 조리법에 대해서도 알 수 있다!
즉, 의존성을 인터페이스로 추상화하여 더 다양한 의존 관계를 맺을 수가 있다.
이렇게 하면 실제 구현 클래스와의 관계가 느슨해지고, 결합도가 낮아진다.
3. 의존성 주입
이제 의존성 주입에 대해 알아보자. 지금까지는 클래스 내부에서 어떤 의존성을 가질지 직접 정했다. 하지만 의존성이 외부에서 정해져 들어오는 상황을 생각해 보자. 위에서 사용했던 예시를 계속해서 사용하겠다.
조 모 군은 든든하게 배를 채우고, 다시 공부를 위해 자리에 앉았다. 하지만 옆에서 롤 승급전을 하던 동생이 갑자기 자기도 배고프다며 다짜고짜 신라면을 끓여달라고 요구한다.
어이가 없는 조 모 군이지만, 동생의 승급전 승리를 위해 신라면을 끓여주기로 하였다...
위 상황을 코드로 표현해 보면 다음과 같다.
public class RamenRecipe {
private Ramen ramen;
public RamenRecipe(Ramen ramen) { // 외부에서 ramen의 종류가 들어오면
this.ramen = ramen(); // 라면 레시피가 결정됨
}
}
private RamenRecipe ramenRecipe = new RamenRecipe(new Sinramen()); // 동생의 신라면을 끓여달라는 요청
동생이 라면의 종류를 신라면으로 정해서 RamenRecipe로 들어왔기 때문에, 라면 레시피는 신라면의 레시피로 결정이 되어버렸다. 즉, RamenRecipe가 의존하는 Ramen이 동생(외부)에 의해서 주입된 것이다.
이처럼 의존성을 외부에서 결정하여 주입하는 것이 의존성 주입이다.
다른 말로 하면 객체의 의존관계를 내부에서 결정하는 것이 아니라 객체 외부에서, 런타임 시점에 결정하는 방식이다.
4. 의존성 주입 방법
- 생성자 주입
public class RamenRecipe {
private final Ramen ramen;
public RamenRecipe(Ramen ramen) { // 생성자 주입
this.ramen = ramen();
}
}
생성자 호출 시점에 딱 1번만 호출되는 것을 보장
불변 : 한번 주입하면 주입된 인스턴스는 변하지 않는 것을 보장
필수 : 주입받는 필드에 final 키워드를 이용함으로써 반드시 주입받아야 되는 것을 보장
- 필드 주입
public class RamenRecipe {
@Autowired
private Ramen ramen;
}
코드가 간결해지는 장점이 있지만 외부에서 변경이 불가능하여 테스트 코드 작성하기가 힘듦
@Autowired가 필요하기 때문에 순수 자바 코드로 테스트 코드를 작성할 수 없음
- 수정자 주입 (대표적으로 Setter)
public class RamenRecipe {
private Ramen ramen;
public void setRamen(Ramen ramen){ // 수정자(Setter) 주입
this.ramen = ramen;
}
}
선택, 변경 가능성이 있는 의존관계에 사용
5. 의존성 주입의 장단점
의존성 주입의 장점
- 하나의 작업만 수행하는 작은 객체는 많은 상황에서 재결합하고 재사용하기가 쉽기 때문에 코드의 재사용성, 유연성 이 커짐
- 객체 간 결합도가 낮기 때문에 한 클래스를 수정했을 때 다른 클래스도 수정해야 하는 상황이 적음
- 유지보수가 쉬우며 테스트가 용이함
- 확장성을 가짐
의존성 주입의 단점
- 책임이 분리되어 있기 때문에 클래스 수가 늘어나게 되고, 이로 인해 복잡성이 증가
- 주입된 객체들에 관한 코드 추적이 어려움
- 초기 개발에 큰 노력이 필요
- DI 프레임워크를 사용하면 빌드 시간이 늘어나며, 프레임워크에 대한 의존도를 높임
출처
https://tecoble.techcourse.co.kr/post/2021-04-27-dependency-injection/
https://hudi.blog/dependency-injection/
'개발 > 스프링 개념' 카테고리의 다른 글
[Spring] Session이란? (0) | 2023.01.25 |
---|---|
[Spring] Select에 @Transaction을 사용하는 이유 (0) | 2023.01.20 |
[Spring] Spring 웹 계층 구조 (0) | 2023.01.20 |
[Spring] 영속성 컨텍스트와 @Transactional 어노테이션 (0) | 2022.11.19 |
[Spring] Open-In-View란? (0) | 2022.11.19 |