[Spring] Spring IoC와 DI란?

Sujung Shin·2023년 1월 15일
0
post-thumbnail

추가 공부가 필요할 것 같아서,
웹 서핑과 여러가지 레퍼런스 자료들을 이용하여 공부하였다.

💡IoC (제어의 역전)

  • IoC란 Inversion of Control의 줄임말이며, 제어의 역전이라고 한다.
  • 스프링 어플레이션에서는 오브젝트(빈)의 생성과 의존관계 설정, 사용, 제거 등의 작업을 스프링 컨테이너가 담당한다.
  • 이를 스프링 컨테이너가 코드 대신 오브젝트의 제어권을 가지고 있다고 해서 IoC라고 부른다.

❤️ IoC 컨테이너

  • 스프링에서는 IoC를 담당하는 컨테이너를 애플리케이션 컨텍스트라고 부른다.
  • 오브젝트의 생성과 오브젝트 사이의 런타임 관계를 설정하는 DI 관점으로 보면, 컨테이너를 Bean factory(빈 팩토리) 혹은 DI 컨테이너라고 부른다.
  • 그러나 스프링 컨테이너는 단순한 DI 작업보다 많은 일을 하는데, DI를 위한 Bean factory에 여러가지 기능을 추가한 것을 애플리케이션 컨텍스트라고 한다.


💡 DI (의존성 주입)

❤️ 의존 관계(Dependency)란?

"A가 B를 의존한다"
= "의존 대상 B가 변하면, A에 영향을 미친다."

즉, B의 기능이 추가되거나 변경되면, 그 영향이 A에 미치는 것이다.
해당 코드를 보자.

class BurgerChef {
    private HamBurgerRecipe hamBurgerRecipe;

    public BurgerChef() {
        hamBurgerRecipe = new HamBurgerRecipe();        
    }
}

햄버거 레시피가 변하게 되었을 때, BugerChef 클래스를 수정해야 한다.
레시피의 변화가 요리사의 행위에 영향을 미쳤기 때문에 BugerchefHamburgerRecipe에 의존한다고 볼 수 있다.

🤔 Dependency Inversion principle

더 중요한 모듈(BurgerChef)이 덜 중요한 모듈(HamburgerRecipe)에 의존하도록 만들면 안된다.
이 관계를 뒤집기 위해서는 추상화가 필요하다.

❤️ 의존 관계를 인터페이스로 추상화

위 예제를 보면, BugerChefHamburgerRecipe만 의존할 수 있는 구조로 되어있다.
더 다양한 햄버거 레시피를 의존할 수 있게 구현하려면 인터페이스로 추상화해야 한다.

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가 되었다. 이처럼 의존관계를 인터페이스로 추상화하게 되면 다양한 의존관계를 맺을 수 있고, 실제 구현 클래스와의 관계가 느슨해져 결합도가 낮아진다.

❤️ DI (의존 관계 주입)이란?

지금까지의 구현에서는 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 컨테이너가 돼서, 빈을 알아서 주입해준다.

❤️ @Autowired 어노테이션

  • DI를 할 때 사용하는 어노테이션이며, 의존 관계 타입에 해당하는 빈을 찾아 주입하는 역할을 한다.
  • 쉽게 말하자면, 스프링 서버가 올라갈 때 애플리케이션 컨텍스트가 @Bean @Service @Controller 어노테이션을 이용하여 등록한 스프링 빈을 생성하고, @Autowired 어노테이션이 붙은 위치에 의존 관계 주입을 수행하게 된다.

스프링에서 의존성을 주입하는 방식에는 현재 3가지가 있다.

1. 생성자 주입(Constructor Based Dependency Injection)
2. Setter 주입(Setter Based Dependency Injection)
3. field 주입 <- 사용하지 않는 것을 권장.

1. 생성자 주입

말 그대로 @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;
}

2. setter 주입

Setter 메서드를 이용하여 주입하는 방식이다.

@Controller
private class MyController {
	
    private MyService myService;
    
    @Autowired
    public void setMyService(MyService myService) {
    	this.myService = myService;
        }
 }



😣 스프링 순환 참조 문제(How to avoid circular reference?)

  • 순환 참조란 서로 다른 여러 빈들이 서로 물고 늘어져서 계속 연결되어 있음을 의미한다.

즉, 아래처럼 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 요청을 받았을 때 힙 메모리가 증가할 수 있으며 메모리가 충분하지 않은 상황이면 장애가 발생할 수 있다.

✔️ setter 주입하여 해결

@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;
    }
}
profile
백문이불여일타

0개의 댓글

관련 채용 정보