추가 공부가 필요할 것 같아서,
웹 서핑과 여러가지 레퍼런스 자료들을 이용하여 공부하였다.
"A가 B를 의존한다"
= "의존 대상 B가 변하면, A에 영향을 미친다."
즉, B의 기능이 추가되거나 변경되면, 그 영향이 A에 미치는 것이다.
해당 코드를 보자.
class BurgerChef {
private HamBurgerRecipe hamBurgerRecipe;
public BurgerChef() {
hamBurgerRecipe = new HamBurgerRecipe();
}
}
햄버거 레시피가 변하게 되었을 때, BugerChef
클래스를 수정해야 한다.
레시피의 변화가 요리사의 행위에 영향을 미쳤기 때문에 Bugerchef
는 HamburgerRecipe
에 의존한다고 볼 수 있다.
더 중요한 모듈(BurgerChef
)이 덜 중요한 모듈(HamburgerRecipe
)에 의존하도록 만들면 안된다.
이 관계를 뒤집기 위해서는 추상화가 필요하다.
위 예제를 보면, BugerChef
는 HamburgerRecipe
만 의존할 수 있는 구조로 되어있다.
더 다양한 햄버거 레시피를 의존할 수 있게 구현하려면 인터페이스로 추상화해야 한다.
class BurgerChef {
private BurgerRecipe burgerRecipe;
public BurgerChef() {
burgerRecipe = new HamBurgerRecipe();
//burgerRecipe = new CheeseBurgerRecipe();
//burgerRecipe = new ChickenBurgerRecipe();
}
}
interface BugerRecipe {
newBurger();
}
class HamBurgerRecipe implements BurgerRecipe {
public Burger newBurger() {
return new HamBerger();
}
}
위 코드에서 볼 수 있듯이, 다양한 버거 레시피에 의존할 수 있는 BurgerChef
가 되었다. 이처럼 의존관계를 인터페이스로 추상화하게 되면 다양한 의존관계를 맺을 수 있고, 실제 구현 클래스와의 관계가 느슨해져 결합도가 낮아진다.
지금까지의 구현에서는 BurgerChef
내부적으로 의존관계인 BurgerRecipe
가 어떤 값을 가질지 직접 정하고 있다.
이때 DI는,
사장님: '어떤 버거'를 만들지?
를 정하는 상황 자체를 말할 수 있다.
즉, BurgerChef
가 의존하고 있는 BurgerRecipe
를 외부(사장님)이 결정하고, 주입하는 것이다.
class BurgerChef {
private BurgerRecipe burgerRecipe;
public BurgerChef(BurgerRecipe bugerRecipe) {
this.burgerRecipe = bugerRecipe;
}
}
//의존관계를 외부에서 주입 -> DI
new BurgerChef(new HamBurgerRecipe());
new BurgerChef(new CheeseBurgerRecipe());
new BurgerChef(new ChickenBurgerRecipe());
이처럼 의존 관계를 외부에서 결정하는 것을 DI(의존 관계 주입)이라고 한다.
스프링에서는 외부의 대상이 IoC 컨테이너가 돼서, 빈을 알아서 주입해준다.
@Bean
@Service
@Controller
어노테이션을 이용하여 등록한 스프링 빈을 생성하고, @Autowired
어노테이션이 붙은 위치에 의존 관계 주입을 수행하게 된다.스프링에서 의존성을 주입하는 방식에는 현재 3가지가 있다.
1. 생성자 주입(Constructor Based Dependency Injection)
2. Setter 주입(Setter Based Dependency Injection)
3.field 주입<- 사용하지 않는 것을 권장.
말 그대로 @Autowired
어노테이션으로 다른 빈을 주입하는 방식이다.
@Controller
public class MyController {
private MyService myService;
@Autowired
public MyController(MyService myService) {
this.myService = myService;
}
}
이때 주입해야 하는 경우가 많을 경우 생성자 자체가 커지게 된다. 그런 경우에는 lombok을 사용하게 되면 해결할 수 있다.
☝️ 클래스 레벨에
@RequiredArgsConstructor
어노테이션 붙이기✌️ 주입할 빈은 final 키워드 붙여주기
@Controller
@RequiredArgsConstructor
public class MyController {
private final MyService mySerivce;
}
Setter 메서드를 이용하여 주입하는 방식이다.
@Controller
private class MyController {
private MyService myService;
@Autowired
public void setMyService(MyService myService) {
this.myService = myService;
}
}
즉, 아래처럼 A는 B에서 필요한데, B는 또 A에서 필요한 상태를 의미한다.
주로 생성자 주입 방식을 채택할 때 많이 발생하는 문제이다.
Bean A -> Bean B -> Bean A...
만약 Bean A -> Bean B -> Bean C 처럼 연결되어있다면, 스프링은 A를 먼저 만들고 A를 필요로 하는 B를 만들고, B를 필요로 하는 C를 만들게 된다.
하지만 순환 참조가 발생하면 스프링은 어느 빈을 먼저 생성해야 할지 결정하지 못하고 순환참조 오류가 발생하게 된다.
이는 결국 설계가 잘못되었음을 의미한다. 오마이갓!
순환 참조의 고리를 끊어버리는 것이다.
설계를 조금만 바꿔서 해결가능한 경우이지만, 설계의 변경이 힘든 경우에는
@Lazy
어노테이션을 붙여서 해결@Component
public class BeanA{
private BeanB beanB;
@Autowired
public BeanA(BeanB beanB) {
this.beanB = beanB;
}
}
즉, 이렇게 되어있었던 코드를
@Component
public class BeanA{
private BeanB beanB;
@Autowired
public BeanA(@Lazy BeanB beanB) {
this.beanB = beanB;
}
}
이렇게 바꿔주면 된다!
하지만, 한계점이 있다.
LAZY 전략은, 앱 기동 시점에가 아닌 실제 해당 빈이 필요한 시점에 빈을 생성하기 때문에(@Eager
과 다른 점), 특정 http 요청을 받았을 때 힙 메모리가 증가할 수 있으며 메모리가 충분하지 않은 상황이면 장애가 발생할 수 있다.
@Component
public class BeanA{
private BeanB beanB;
@Autowired
public setBeanA(BeanB beanB) {
this.beanB = beanB;
}
}
@Component
public class BeanB{
private BeanA beanA;
@Autowired
public setBeanB(BeanA beanA) {
this.beanA = beanA;
}
}