IoC(Inversion of Control, 제어의 역전)
객체 생성 및 의존성 주입 등의 제어를 개발자가 아닌 프레임워크가 담당하도록 하는 설계 원칙이다.
사용할 객체를 직접 생성하지 않고, 객체 생명주기 관리를 외부에 위임한다.
이는 애플리케이션의 구조를 더 유연하고 테스트 가능하게 만든다.
즉, IoC의 핵심 개념은 애플리케이션의 제어 흐름을 프레임워크가 관리한다는 것이다.
이를 통해 객체 간 결합도를 낮추고, 코드의 재사용성과 유연성을 높인다.
스프링에서의 IoC
스프링에서는 스프링 컨테이너가 오브젝트인 빈(Bean)의 생성, 의존 관계 설정 등의 작업을 스프링 컨테이너가 수행한다.
스프링에서 IoC, 제어의 역전은 코드 대신 객체에 대한 제어권을 스프링 컨테이너에 넘겨 스프링 컨테이너가 흐름을 제어하게 된다.
따라서 스프링 컨테이너를 IoC 컨테이너라고도 부른다.
- 아래 예제는 A 클래스에서 B 필드를 가지고 있으며 생성자에서 직접 객체를 생성하여 필드를 초기화한다.
- 객체 생명주기 및 메서드 호출을 개발자가 직접 제어하고 있는 상황이다.
public class A {
private B b;
public A() {
b = new B();
}
}
- B라는 객체가 스프링 컨테이너에서 관리하는 Bean이라면 @Autowired를 통해 객체를 주입받을 수 있다.
- 개발자가 직접 객체를 관리하는 것이 아닌, 스프링 컨테이너에서 객체를 생성하여 객체를 주입시켜주었다.
- 이것이 IoC, 제어의 역전이며, 프로그램 제어권이 역전된 것이다.
public class A {
@Autowired
private B b; // 필드 주입
}
DI(Dependency Injection, 의존성 주입)
의존 관계란?
DI는 의존성 주입 또는 의존 관계 주입을 말한다. 여기서 의존 관계란, A가 B에 의존한다.라는 문장에서 의미를 파헤쳐볼 수 있다.
By 토비의 스프링 3.1, 의존 대상인 B가 변하면 그것이 A에 영향을 미친다.
B의 기능이 변화함에 따라 A에 영향을 미치며, 이를 A가 B에 의존한다고 말할 수 있다.
의존 관계 주입(의존성 주입)이란?
의존성 주입 : 사용할 객체를 직접 생성하지 않고, 외부 컨테이너가 생성한 객체를 주입받아 사용하는 방식
의존 관계를 객체 외부에서 결정해주는 것이다. 즉, 외부에서 두 객체 간의 관계를 결정해주며, 프로그램 내에서는 각 구현체는 구체화가 아닌 추상화에 의존하게 된다. 즉, 객체가 아니라 인터페이스에 의존한다.
- 애플리케이션을 실행하는 Runtime에 외부에서 실제 구현체를 만들고 클라이언트에 전달한다.
- 외부인 AppConfig에서 객체 인스턴스 생성을 하고, 참조 값을 전달해서 연결한다.
- 이를 통해 클라이언트 코드 변경을 하지 않고, 클라이언트가 호출하는 구현체, 구체화 종류를 변경할 수 있다.
- 즉, 정적인 클래스 의존 관계는 변경하지 않고(=코드 변경 X), 동적인 의존 관계를 쉽게 변경할 수 있게 된다.
의존성 주입이 필요한 이유
- 아래에 커피를 판매하는 CafeService Class와 AmericanoRecipe라는 커피 레시피가 있다.
@Service
public class CafeService {
private AmericanoRecipe americanoRecipe;
public CafeService() {
this.americanoRecipe = new AmericanoRecipe();
}
}
위 예제의 문제점
- 강하게 결합된 CafeService Class와 AmericanoRecipe Class
- 만약, CafeService Class에서 LatteRecipe Class를 활용하여 다른 커피 메뉴를 판매하려면 CafeService Class의 생성자를 수정해야 한다.
- 만약, CafeService Class는 AmericanoRecipe Class에 의존하기 때문에 AmericanoRecipe 생성자에 변경이 생긴다면 CafeService Class 또한 변경이 되어야 한다. 또한, CafeService Class가 사용된 모든 곳에 영향을 받으며, 유연성이 떨어진다.
- 객체와의 관계 X, 클래스 간의 관계 O
- 다른 객체의 구체 클래스( AmericanoRecipe , LatteRecipe)를 모르더라도 Class가 Interface를 구현했다면 Interface 타입으로 사용 가능하다.
의존성 주입 3가지 방법
- 생성자 주입(Constructor Injection) (*추천)
- 수정자 주입(Setter Injection)
- 필드 주입(Field Injection)
스프링은 @Autowired 어노테이션으로 의존성 주입을 명시한다.
(*스프링 4.3 이후는 생성자 주입에서 생성자가 1개이면 @Autowired 생략 가능)
1. 생성자 주입(Constructor Injection)
@Service
public class CafeService {
private final AmericanoRecipe americanoRecipe;
@Autowired
public CafeService(AmericanoRecipe americanoRecipe) {
this.americanoRecipe = new AmericanoRecipe();
}
}
- 생성자를 통해 의존성을 주입하며 생성자 호출 시 딱 1번만 호출되는 것을 보장한다.
- 생성자가 1개만 존재하면 @Autowired 생략해도 자동 주입된다.
- 주입받은 객체가 변하지 않거나 반드시 객체 주입이 필요하면 강제하기 위해 사용할 수 있다.
- 주입받을 field를 불변 보장 final 키워드로 선언 가능하다.
- NPE(NullPointException)을 방지할 수 있다.
- 순환 참조를 컴파일 단계에서 찾을 수 있다.
- 테스트 코드 작성에 용이하다.
- 순환 참조 감지 기능을 스프링에서 제공하며, 순환 참조 시 에러를 보여준다.
- 생성자 인자가 많아지면 코드가 길어질 수 있다.
1-1. 생성자 주입 + Lombok 라이브러리 활용 (*가장 추천)
@Service
@RequiredArgsConstructor
public class CafeService {
private final AmericanoRecipe americanoRecipe;
}
- 개발 편의성 라이브러리인 Lombok에서 @RequiredArgsConstructor를 활용하면 더욱 간결하게 만들 수 있다.
- @RequiredArgsConstructor 어노테이션은 NotNull이거나 final이 붙은 변수들에 대해 생성자를 만들어준다.
2. 수정자 주입(Setter Injection)
@Service
public class CafeService {
private AmericanoRecipe americanoRecipe;
@Autowired
public void setAmericanoRecipe(AmericanoRecipe americanoRecipe) {
this.americanoRecipe = new AmericanoRecipe();
}
}
- 생성자 주입과는 다르게 주입받는 객체가 변경될 가능성이 있으면 사용한다.
- 선택과 변경 가능성이 있을 때 사용하며, Setter는 언제든 변경의 위험이 있다.
- @Autowired가 없으면 컴파일은 되지만 실행할 때 NPE(NullPointException)가 발생한다.
- final 키워드 선언이 불가능하다.
- 한번 주입이 되면 변경 가능성이 거의 없기에 사용을 지양하자.
- 순환 참조 문제가 발생할 수 있다.
3. 필드 주입(Field Injection)
@Service
public class CafeService {
@Autowired
private AmericanoRecipe americanoRecipe;
}
- 필드에 바로 의존성을 주입하는 방법이다.
- 코드가 짧아진다.
- 외부 변경이 불가능해서 Test가 어렵다.
- 애플리케이션 실제 코드와 상관없는 특정 Test를 할 때 사용한다.
- 하나의 클래스가 여러 책임(기능)을 갖게 될 가능성이 크고, 의존 관계를 파악하기에 어렵다.
- DI 컨테이너와 결합도도 커진다.
- 불변성 보장이 없다.
- 순환 참조가 발생할 수 있다.
순환 참조란?
@Service
public class CafeServiceImpl implements CafeService {
private final ShopService shopService;
@Autowired
public CafeServiceImpl(ShopService shopService) {
this.shopService = shopService;
}
@Override
public void cafeMethod() {
shopService.shopMethod();
}
}
@Service
public class ShopServiceImpl implements ShopService {
private final CafeService cafeService;
@Autowired
public ShopServiceImpl(CafeService cafeService) {
this.cafeService = cafeService;
}
@Override
public void shopMethod() {
cafeService.cafeMethod();
}
}
- ShopServiceImpl의 shopMethod()는 CafeServiceImpl의 cafeMethod()를 호출한다.
- CafeServiceImpl의 cafeMethod()는 ShopServiceImpl의 shopMethod()를 호출한다.
- 서로 호출을 반복하면서 결국 StackOverflowError를 발생시키고 죽게 된다.
- 수정자 주입, 필드 주입은 객체(빈)를 생성하고 비즈니스 로직 상에서 순환 참조가 일어나며, 즉, 컴파일 단계에서 순환 참조를 잡아낼 수 없다.
- 생성자 주입은 스프링 컨테이너가 객체(빈) 생성 시점에 순환 참조를 확인하며, 즉, 컴파일 단계에서 순환 참조를 잡을 수 있다.
참고 자료
https://mozzi-devlog.tistory.com/18
https://mangkyu.tistory.com/150
'Spring' 카테고리의 다른 글
[Spring] 스프링 싱글톤 컨테이너(Singleton Container) (0) | 2024.06.06 |
---|---|
[Spring] POJO(Plain Old Java Object)란? (1) | 2024.06.06 |
[Spring] 스프링의 삼각형(IoC/DI, AOP, PSA) (0) | 2024.06.05 |
[Spring] 스프링 컨테이너(Spring Container), 스프링 빈(Bean), 빈 설정 방법(XML, Java 기반, Annotation 기반) (0) | 2024.06.04 |
[Spring] 스프링 프레임워크(Spring Framework)란? (0) | 2024.06.02 |