의존성 주입
1. 의존성 주입(DI)의 필요성
다음과 같은 주문 서비스를 만든다고 하자.
[주문 서비스 역할 관계도]
그림과 같이 주문 서비스는 회원 저장소와 할인 정책을 의존해서 역할을 수행한다.
스프링 핵심 원리 게시글은 위의 서비스를 제공하는것을 전제로 각종 예시를 설명할 예정이다.
[주문 서비스 메서드 명 관계도]
위의 관계도는 주문 서비스에 사용되는 실질적인 클래스와 인터페이스 명과 의존관계를 나타낸다.
위의 관계도에서 알 수 있듯이 주문 서비스를 구현한 OderServiceImpl 클래스는 회원 저장소 역할의 MemberRepository와 할인정책 역할을 하는 DiscountPolicy의 인터페이스를 의존한다.
관계만 볼 경우 '추상화에 의존한다'라는 DIP와, 확장은 가능하며 변경은 불가인 OCP를 잘 준수 하는 것처럼 보인다.
그러나 DiscountPolicy를 FixDiscountPolicy에서 RateDiscountPolicy로 변경할 경우 기존 방식으로는 여러 문제가 발생 한다.
[기존 코드로 할인 정책의 변경]
public class OrderServiceImpl implements OrderService {
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
애초에 클래스 내에서 구현 객체를 의존하고 있으므로 DIP가 지켜지지도 않았으며, 이 때문에 구현 객체를 변경해야 할 때 코드의 변경이 불가피하다.
즉, 위와 같은 의존 방식을 고수할 경우 DIP와 OCP를 준수 할 수 없다.
이 때문에 DIP와 OCP를 준수하기 위해서, 클래스 내부가 아니라 외부에서 의존성 주입을 할 필요가 있다.
[의존성 주입 역할의 AppConfig]
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(
memberRepository(),
discountPolicy()); }
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
위의 AppConfig 클래스는 구현 객체를 생성하고, 연결하는 역할을 한다.
즉, 이제 OrderServiceImpl과 같은 객체는 자신의 로직만 수행할 뿐 더이상 스스로 구현 객체를 생성하거나 연결하지 않는다.
이 때문에 AppConfig에 설정된 객체들은 구현 객체에 의존할 필요가 없게된다.
[추상화에 의존하는 OrderServiceImpl]
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
.
.
.
}
위의 코드처럼 이제 OrderServiceImpl 클래스는 구현 객체가아니라 생성자로 인터페이스만을 받고 인터페이스에만 의존한다.
OrderServiceImpl은 MemberRepository와 DiscountPolicy를 호출하지만 정확히 어떤 구현 객체가 주입될지는 알 수 없다.
해당 역할은 오로지 AppConfig 클래스에서 수행되기 때문에 이제 OrderServiceImpl은 자신의 로직을 수행하는 것에 집중 할 수 있다.
OderServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고 해서 이를 의존성 주입(Dependency Injection)이라고 한다.
[실제 사용 예시]
public class OrderApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
OrderService orderService = appConfig.orderService();
.
.
.
}
OderService를 사용해야한다면 AppConfig 객체를 생성후 OderService를 생성하는 메서드를 불러오면 된다.
[구현 객체 변경 예시]
public class AppConfig {
.
.
.
public DiscountPolicy discountPolicy() {
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
AppConfig를 쓰는 방식에서 할인 정책을 FixDiscountPolicy -> RateDiscountPolicy로 변경할 경우 이제 OderServiceImpl의 코드를 수정할 필요가 없다.
이제 구현 객체를 변경할 경우 AppConfig에서 DiscountPolicy를 생성하는 메소드를 변경하기만 하면 된다.
IOC
1. IOC란
기존 프로그램은 구현 객체가 스스로 필요한 구현 객체를 생성, 연결, 실행 하였다.
즉, 구현 객체가 프로그램의 제어 흐름을 스스로 조종하였다.
그러나 의존성을 주입해주는 Appconfig 방식을 이용할 경우 구현 객체는 자신의 로직을 실행하는 역할만 담당할 뿐, 스스로 구현 객체를 선택하고, 생성할 필요가 없다.
이처럼 제어의 흐름을 직접 제어하는 것이아니라 외부에서 관리하는 것을 제어의 역전(IOC)라고 한다.
스프링 컨테이너
1. 스프링 기반의 DI와 IOC
기존에는 개발자가 Appconfig를 사용해서 직접 객체를 생성하고 DI를 했지만, 스프링을 사용할 경우 스프링 컨테이너가 이 역할을 대신한다.
[스프링 컨테이너 수동 등록 방식]
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(
memberRepository(),
discountPolicy());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
스프링 컨테이너는 @Configuration이 붙은 클래스를 설정정보로 사용한다.
여기서 @Bean이 붙은 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다.
이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라고 한다.
※스프링 빈은 @Bean이 붙은 메서드의 이름을 스프링 빈의 이름으로 사용한다(조회할 때 사용하는 이름).
[스프링 컨테이너 사용 예시]
public class OrderApp {
public static void main(String[] args) {
// AppConfig appConfig = new AppConfig();
// MemberService memberService = appConfig.memberService();
// OrderService orderService = appConfig.orderService();
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
.
.
.
}
}
ApplicationContext는 스프링 컨테이너이다. 때문에 스프링 컨테이너를 조회하기 위해서는 ApplicationContext를 통해야 한다.
위의 예제와 같이 스프링 빈은 getBean()메서드를 통해서 applicationContext.getBean("빈 이름", 타입.class)과 같은 형식으로 조회 할 수 있다.
※ AnnotationConfigApplicationContext는 ApplicationContext외에도 다양한 인터페이스를 상속한 구현체이며, 스프링 컨테이너를 다루는데 더 다양한 기능을 추가한 클래스이다.
'Spring > Spring 핵심 원리' 카테고리의 다른 글
#6 의존관계 자동 주입 (0) | 2021.05.18 |
---|---|
#5 컴포넌트 스캔 (0) | 2021.05.14 |
#4 싱글톤 컨테이너 (0) | 2021.05.14 |
#3 스프링 빈 조회 (0) | 2021.05.14 |
#1 객체 지향 설계의 5가지 원칙(SOLD)과 Spring (0) | 2021.05.13 |
댓글