현재 AppConfig 클래스의 경우, 중복이 존재하고 역할에 따른 구현이 보이지 않는 구조이다.
아래의 그림과 같이 명확히 구현을 나누어서 리팩토링을 해야한다.
리팩토링 전의 코드
package hello.core;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(
new MemoryMemberRepository(),
new FixDiscountPolicy());
} }
리팩토링 후의 코드
package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
public MemoryMemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public DiscountPolicy discountPolicy() {
return new FixedDiscountPolicy();
}
}
수정 후의 AppConfig 코드를 보면 역할과 구현이 한번에 나누어져서 애플리케이션의 전체 구성이 어떻게 되어있는지 빠르게 파악 할 수 있다.
새로운 구조와 할인 정책 적용
이제 정액할인 정책을 정률 할인 정책으로 바꿔야 한다.
위 코드의 FixedDiscountPolicy를 RateDiscountPolicy로 바꾸면 된다.
왜 이렇게 간단한 수정으로 할인 정책을 변경 할 수 있을까?
우리는 사용영역(구현체 및 인터페이스)과 구성영역(AppConfig)을 철저하게 분리시켜 코드를 구현했기에 가능하다.
구현(정책)을 변경하고 싶을때, 사용영역의 경우 단 한줄의 코드도 변경이 없어도 되며 오직 구성영역의 코드만 바꾸면 구현(정책)이 변경된다.
SRP,DIP,OCP의 적용
SRP - 단일 책임 원칙
클라이언트 객체는 직저 구현 객체를 생성하고, 연결하고 실행하는 다양한 책임을 가지고 있다.
SRP 단일 책임 원칙을 따르면서 서로의 관심사(책임)을 분리시켰다.
구현 객체를 생성하고 연결하는 책임은 AppConfig가 담당한다.(구성영역)
클라이언트 객체는 오직 실행하는 책임만 담당한다.(사용영역)
DIP - 의존관계 역전 원칙
의존성 주입은 해당 원칙을 따르는 방법 중 하나이다.
모든 클라이언트 코드가 추상화 인터페이스에만 의존하도록 코드를 설정했다.
하지만 클라이언트 코드는 인터페이스만으로는 애플리케이션을 실행할 수 없다.(NPE 발생)
AppConfig가 객체 인스턴스(구현체)를 클라이언트 코드 대신 생성해서 클라이언트 코드에 의존관계를 주입한다.
해당 방식을 통해 DIP 원칙도 지키고, 어플리케이션 실행도 가능하게 한다.
OCP - 소프트웨어 요소는 확장에는 열려 있으나, 변경에은 닫혀 있어야 하는 원칙
애플리케이션을 사용영역과 구성영역으로 나누었다.
AppConfig만을 변경함으로써 클라이언트 코드의 변경 없이 새로운 기능을 확장할 수 있다.(해당 예제에서는 정액할인정책에서 정률할인정책으로의 확장)
소프트웨어요소를 새롭게 확장해도 사용 영역의 변경은 닫혀 있다.
IoC, DI 그리고 컨테이너
IoC(Inversion of Control) - 제어의 역전
기존의 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고,연결하고,실행했다. 한 마디로 구현객체가 스스로 프로그램의 제어 흐름을 제어했다.
하지만 AppConfig를 구현하고 난 다음부터는, 클라이언트 구현객체는 오직 실행 역할만 담당하고 있다.즉, 프로그램의 제어 흐름에 대한 역할은 AppConfig가 가지고 있다.예를 들어 이제는 OrderServiceImpl 은 본인이 어떠한 구현체를 선택할지 더 이상 알 수 없다. 동시에 OrderServiceImpl 는 묵묵히 자신의 코드만 실행하는 역할을 하고 있다.
이렇듯 프로그램의 제어 흐름을 해당 객체가 직접 제어하는것이 아니라 외부에서 관리하는 것을 제어의 역전이라고 한다.
DI(Dependency Injection) - 의존성 주입
OrderServiceImpl는 DiscountPolicy 인터페이스에 의존한다.하지만 실제로 어떤 구현 객체가 스스로에게 사용될지 모른다.
의존 관계는 정적인 클래스 의존관계와 실행 시점에 결정되는 동적인 객체 의존관계로 나누어서 접근해야한다.
정적인 클래스 의존관계
클래스가 사용하는 import된 코드만 보고도 의존관계를 파악할 수 있는 경우다.
즉, 애플리케이션을 실행하지 않아도 파악이 가능하다.
동적인 객체 인스턴스 의존관계
애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계이다.
애플리케이션 실행시점에 외부에서 실제 실행 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는것을 의존관계 주입이라고 한다.
외부에서 객체 인스턴스를 생성하고, 그 참조값을 전달해서 연결된다.
의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고,클라이언트가 호출하는 대상이 타입 인스턴스를 변경 할 수있다.
또한 정적인 클래스 의존관계를 변경하지않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경 할 수 있다.
컨테이너
AppConfig처럼 객체를 생성하고 의존관계를 연결해주는 것을 IoC컨테이너 또는 DI컨테이너라고 한다.
의존관계 주입에 초점을 둬 최근에는 주로 DI컨테이너라고 부른다.
혹은 어셈블러,오브젝트 팩토리 등으로도 불린다.
스프링으로 전환하기
AppConfig 스프링 기반으로 변경하기
package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public MemoryMemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
@Configuration : AppConfig에 설정을 구성해주는 어노테이션
@Bean : 스프링 컨테이너에 스프링 빈으로 등록하는 어노테이션
MemberApp에 스프링 컨테이너 적용
MemberApp 클래스는 순수 자바코드로 테스트 구현한 클래스이다.
memberService가 잘 동작하는지 확인하는 동작을 수행한다.
현재는 스프링을 적용해서 코드를 변경했다.
package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class MemberApp {
public static void main(String[] args) {
//AppConfig appConfig = new AppConfig();
//MemberService memberService = appConfig.memberService();
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
Member member = new Member(1L, "brido", Grade.VIP);
memberService.join(member);
Member findMember = memberService.findMember(1L);
System.out.println("findMember = " + findMember.getName());
System.out.println("member = " + member.getName());
}
}
OrderApp에 스프링 컨테이너 적용
위와 마찬가지로 OrderService가 잘 동작하는 테스트하는 코드이다.
package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.order.Order;
import hello.core.order.OrderService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
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);
Member member = new Member(1L, "brido", Grade.VIP);
memberService.join(member );
Order order = orderService.createOrder(1L, "gun", 5000);
System.out.println("order = " + order);;
}
}
스프링 컨테이너
위 코드에서 ApplicationContext를 스프링 컨테이너라 한다.
이전 코드는 개발자가 직접 AppConfig를 객체 생성 및 할당 후, DI를 진행했지만 변경된 코드는 스프링 컨테이너를 통해서 진행된다.
스프링 컨테이너는 @Configuration 어노테이션이 붙은 AppConfig를 설정 정보로 사용한다. 또한 @Bean 어노테이션이 붙은 메서드를 모두 호출해서 반환된 객체들을 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체들을 스프링 빈이라고 한다.
스프링 빈은 @Bean이 붙은 메서드명을 스프링 빈의 이름으로 사용한다.(memberService,orderService)
스프링빈은 applicationContext.getBean()메서드를 이용해서 찾을 수 있다.
기존에 개발자가 직접 자바코드로 모든 것을 진행했다면, 이제부터는 스프링 컨테이너에 객체를 스프링 빈으로 등록하고 스프링 컨테이너에서 스프링 빈을 찾아서 사용하도록 변경되었다.