1-1 의존성이란?
의존성이란 흔히 A가 B를 의존하는 것을 말한다. 즉 클래스 A가 클래스 B의 메서드를 호출하거나 클래스 B의 객체를 생성하여 사용하는 경우 A는 B에 의존성을 가지고있다 라고 한다.
2. 의존성 주입(DEpendency Injection)이란?
DI(Dependency Injection)란 의존성 주입, 의존 관계 주입 이라고 불리며, 객체 내부에서 직접 호출하는 대신, 외부(스프링 컨테이너)에서 객체를 생성해서 넣어주는 방식이다.
의존성 주입을 함으로써 객체 간 유연성이 높아지 결합도를 낮출 수 있다. 코드로 한번 살펴보자
아래의 코드는 Cafe클래스와 Americano클래스의 의존성이 높은 코드이다. 의존성이 높은 이유는
1. 객체 생성의 책임 > Americano객체의 내부 구현에 "의존"하는 중이다.
- Cafe클래스의 생성자에서 Americano객체를 생성해서 사용하고 있기 때문에 만약 Americano의 생성자가 변경되면 Cafe에서 사용한 Americano의 생성자 코드 또한 변경해 줘야 한다.
2. 유연성 낮음
- 만약 아메리카노가 아닌 다른 음료를 주문받고 싶으면 Cafe코드를 다시 수정해줘야 한다. 유연성이 낮으며 코드 수정이 번거로워진다.
public class Cafe {
private Americano americano;
public Cafe() {
//Americano의 생성자 변경 시 코드 수정 필요
this.americano = new Americano();
}
public Americano receiveOrder() {
//아메리카노밖에 못받음
return americano.receipt();
}
}
public class Americano {
public Americano receipt() {
return new Americano();
}
}
이 코드를 조금 더 유연하게, 의존성이 낮게 바꿔보면 아래와 같이 바꿀 수 있다.
public class Cafe {
private Coffee coffee;
public Cafe(Coffee coffee) {
this.coffee = coffee;
}
public Coffee receiveOrder() {
return coffee.receipt();
}
}
public interface Coffee {
Coffee receipt();
}
public class Americano implements Coffee {
public Coffee receipt() {
return new Americano();
}
}
public class Latte implements Coffee {
public Coffee receipt() {
return new Latte();
}
}
아래의 코드가 이전의 코드보다 더 좋은 이유는
1. 의존성 주입
- 객체 생성이 외부에서 이루어진 후 주입(파라미터로 들어옴)되고 있기 때문에 의존성이 낮아지게 된다.
2. 인터페이스 활용 - 다형성 증가
- Coffee라는 인터페이스를 사용함으로써 다형성을 높여 이전 코드에서는 아메리카노만 받을 수 있었고 다른 메뉴를 받으려면 Cafe클래스의 코드를 변경해줘야 했지만 여기서는 Coffee 인터페이스를 구현한 다른 음료를 받을 수 있게 되었다.
3. 의존성 주입 방법
1. 필드 주입(Field Injection)
public class Cafe {
@Autowired
private Coffee coffee;
}
@Autowired 어노테이션만 붙혀주면 사용할 수 있어서 편리하다. 하지만 필드 주입에는 여러가지 단점이 있으며 스프링에서도 필드 주입을 권장하지 않고 있다.
필드주입의 단점
1. final을 사용하지 못해 불변성을 보장할 수 없다.
- final 제어자를 사용하지 못하기 때문에 런타임 과정에서 Coffee가 변경 될 가능성이 존재한다. 만약 Coffee라는 객체를 개발자가 실수로 할당한다고 하면 final 제어자가 붙어있으면 코드 작성 과정에서 확인 할 수 있지만 필드주입은 default로 선언되어있기 때문에 어디서 어떻게 변하는지 확인이 불가능 하다.
2. 의존성 숨김 문제(Dependency Hiding)
- A가 B를 필드 주입해서 사용하고 있을 때 내부에서 객체를 직접 생성하기 때문에 B의 관점에서는 명시적으로 알 수 없기때문에 가독성이 떨어진다. > 만약 생성자를 활용하면 내가 객체를 생성해서 넣어주기 때문에 어떤 객체를 사용했는지 확실 하게 알 수 있으며 인터페이스를 사용하더라도 알기 쉬워진다.
3. 자바 기반 테스트 코드에서의 문제
- 필드주입은 스프링 프레임워크(@Autowired)의 도움이 없이는 의존관계 주입을 할 수 없기 때문에 순수 자바 코드로 단위 테스트로 실행을 할 때 의존성 주입이 안돼서 NullPointerException이 발생할 수 있다.
4. 순환 참조 문제
- A가 B를 참조하고 동시에 B가 A를 참조할 때 이것을 순환 참조라고 한다. 무한루프라고 생각하면 편하다. 필드 주입에서는 순환 참조 문제가 런타임시에 발견되어 문제가 생긴다. 이를 해결하는 방법은 생성자 주입 부분을 참고하자
2. 수정자 주입(Setter Injection)
public class Cafe {
private Coffee coffee;
@Autowired
public void setCoffee(Coffee coffee) {
this.coffee = coffee;
}
}
생성자 주입의 단점
1. 불변성을 보장할 수 없다.
- 필드 주입과 동일하게 setter메소드는 public 제어자로 선언하기 때문에 의존성을 주입받는 객체의 변경 가능성이 열리게 된다.
2. 의존성 주입 시점을 알기 어렵다.
- Setter메서드를 통해 의존성이 주입되므로 언제 의존성이 주입되는지 알기 어렵다.
3. 의존성 누락
- 필드 주입과 마찬가지로 의존성이 있는 객체가 생성되지 않아도 이를 포함하는 객체를 생성 가능하여 디버깅 단계에서 체크를 못하고 런타임 시점에서 오류를 발견할 수 있다.
3. 생성자 주입 (Constructor Injection)
public class Cafe {
private final Coffee coffee;
//@Autowired(생략가능)
public Cafe(Coffee coffee) {
this.coffee = coffee;
}
}
Or ========================================================
@RequiredArgsConstructor
public class Cafe {
private final Coffee coffee;
}
생성자가 1개일 때 @Autowired 어노테이션을 생략 가능하다.
@RequiredArgsConstructor 어노테이션을 사용해서 생성자를 만들수도 있다.
생성자 주입의 장점
1. 불변성
- 의존성을 주입받는 객체를 선언할 때 final 제어자를 사용하여 선언하기 때문에 런타임 중에 의존성을 주입받는 객체가 변할 가능성이 없어지게 된다.
2. 테스트 코드 작성의 편리함
- 생성자 주입을 하게 되면 new연산을 통해 직접 객체를 생성한 후 사용하면 되기 때문에 테스트 코드 작성이 편리하다.
3. NullPointerException
- 의존성을 객체 생성 시점에 주입하기 때문에 개발자가 의도적으로 null을 넣지 않는 이상 NullPointerException을 방지할 수 있다.
4. 정리
의존성을 주입할 때에는 생성자 주입을 사용하자. 그 이유는
1. final로 선언하기 때문에 불변성을 유지할 수 있고
2. 의존성을 생성 시점에서반드시 넣어야 하기 때문에 Null참조 및 NullPointerException을 방지할 수 있고
3. 테스트 코드 작성 시 손쉽게 작성할 수 있고
4. 코드 가독성 및 명시성이 높아지고 외부에서 의존성을 주입하기 때문에 의존성을 변경하기 쉬워 코드 유지보수가 쉬워진다.
참고한 블로그
https://yaboong.github.io/spring/2019/08/29/why-field-injection-is-bad/
https://mangkyu.tistory.com/125
스스로 찾아보고 공부한 내용을 기록하는 개인 공부 블로그입니다. 내용 중 틀린부분이나 궁금한 부분은 언제든지 댓글로 남겨주세요🧐