1. 새로운 할인 정책 개발
- 새로운 할인 정책
- 고정 금액 아닌 주문한 금액의 %를 할인해주는 정률 할인으로 변경 - RateDiscountPolicy
# 정률 할인 코드 추가
public class RateDiscountPolicy implements DiscountPolicy{
private int discountPercent = 10;
@Override
public int discount(Member member, int price) {
if(member.getGrade() == Grade.VIP){
return price * discountPercent / 100;
} else {
return 0;
}
}
}
# 정률 할인 테스트
@Test
@DisplayName("VIP가 아니면 할인이 적용되지 않아야 한다")
void vip_x() {
//given
Member member = new Member(2L, "memberBASIC", Grade.BASIC);
//when
int discount = discountPolicy.discount(member, 10000);
//then
assertThat(discount).isEqualTo(0); // 앞이 실제, 뒤가 기댓값
}
2. 새로운 할인 정책 적용과 문제점
- 문제점
- 할인정책 변경 위해서는, OrderSercviceImpl 코드 수정 필요
- DIP 위배 : 추상인터페이스(DiscountPolicy)와 구현클래스(RateDiscountPolilcy)에 모두 의존
- OCP 위배 : 기능 확장하면, 클라이언트 코드도 변경됨
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
- 해결법 1 (실패)
- 인터페이스에만 의존하도록 의존관계 변경? → 구현체가 없어서 NPE(Null Point Exception) 발생
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
// private final DiscountPolicy discountPolicy = new RateDiscountPolicy(); // 구현 클래스에도 의존
private DiscountPolicy discountPolicy; // 인터페이스에만 의존
- 해결법 2 (성공)
- 클라이언트인 OrderServiceImpl에 DiscountPolicy의 구현 객체를 대신 생성하고 주입해야 함
3. 관심사의 분리
- AppConfig
- 애플리케이션 전체 동작 방식을 구성하기 위해 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스
# AppConfig
public class AppConfig { // 애플리케이션의 전체를 설정하고 구성
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService(){
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
- MemberServiceImpl의 생성자 주입 = DI
- MemberServiceImpl의 생성자를 통해 어떤 구현 객체를 주입할지 외부(AppConfig)에서 결정
- MemberServiceImpl은 MemberRepository 추상에만 의존 ⇒ DIP 완성 = 관심사 분리
# MemberServiceImpl - 생성자 주입
public class MemberServiceImpl implements MemberService{
// private final MemberRepository memberRepository = new MemoryMemberRepository();
private final MemberRepository memberRepository; // 추상인터페이스만 의존
// AppConfig 외부파일에서 구현 클래스 설정
public MemberServiceImpl(MemberRepository memberRepository) {
// 생성자를 통해 어떤 구현 객체가 들어올지(주입될지) 알 수 없음 -> 외부(AppConfig)에서 결정
this.memberRepository = memberRepository;
}
@Override
public void join(Member member) {
// 의존관계에 대한 고민은 외부에 맡기고 '실행에만 집중'
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
- OrderServiceImpl 생성자 주입
- OrderServiceImpl의 생성자를 통해 어떤 구현 객체를 주입할지 외부(AppConfig)에서 결정
- OrderServiceImpl은 MemberRepository, DiscountPolicy 추상에만 의존 ⇒ DIP 완성 = 관심사 분리
public class OrderServiceImpl implements OrderService{
// private final MemberRepository memberRepository = new MemoryMemberRepository();
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
// private final DiscountPolicy discountPolicy = new RateDiscountPolicy(); // 구현 클래스에도 의존
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy; // 인터페이스에만 의존, final은 기본 또는 생성자로 할당 해야 함
// DIP 준수
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
- 테스트
# OrderService, MemberService 메인 테스트
public class OrderApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
OrderService orderService = appConfig.orderService();
// MemberService memberService = new MemberServiceImpl();
Long memberId = 1L;
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
Member findMember = memberService.findMember(1L);
System.out.println("new member = " + member.getName());
System.out.println("findMember = " + findMember.getName());
Order order = orderService.createOrder(memberId, "itemA", 20000);
System.out.println("order = " + order);
}
}
# MemberService 테스트 - 관심사 분리
class MemberServiceTest {
MemberService memberService;
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
}
}
# OrderService 테스트 - 관심사 분리
class OrderServiceTest {
MemberService memberService;
OrderService orderService;
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
}
}
4. AppConfig 리팩터링
- 중복 존재, 역할에 따른 구현 잘 안보임
- 리팩터링 전
# 중복 존재
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
- 리팩터링 후
- new MemoryMemberRepository() 중복 제거 → memberRepository() 생성하여 삽입
new FixDiscountPolicy() 중복 제거 → discountPolicy() 생성하여 삽입
⇒ 다른 구현체로 변경 시, 한 부분만 변경하면 됨
# 중복 제거
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();
}
}
5. 새로운 구조와 할인 정책 적용
- FixDiscountPolicy → RateDiscountPolicy로 변경
- 사용 영역 변경 X, 구성 영역만 변경 O
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();
return new RateDiscountPolicy();
}
}
6. 좋은 객체 지향 설계의 5가지 원칙의 적용
- SRP 단일 책임 원칙 (= 한 클래스는 하나의 책임만 가져야 함)
- 클라이언트 객체가 직접 구현 객체 생성, 연결, 실행하는 다양한 책임 → 클라이언트 객체는 실행만 담당
- AppConfig가 구현 생성 및 연결 담당 - DIP 의존관계 역전 원칙 (= 추상화에 의존해야지, 구체화에 의존하면 안 됨)
- 새로운 할인 정책 적용 시, 클라이언트 코드 변경 해야 함(추상화, 구체화 모두 의존) → 추상화 인터페이스에만 의존
- OrderServiceImpl이 FixDiscountPolicy, RateDiscountPolicy와 같은 구현 클래스에 의존 → AppConfig가 구현 객체를 대신 생성하여 클라이언트트 코드에 의존관계 주입 = 추상화 인터페이스에만 의존 - OCP (= 확장에는 개방, 변경에는 닫혀 있어야 함)
- 애플리케이션을 사용 영역과 구성 영역으로 나눔
- AppConfig가 의존관계를 FixDiscountPolicy → RateDiscountPolicy로 변경 시, 클라이언트 코드에 주입
⇒ 클라이언트 코드 변경 X
7. IoC, DI, 그리고 컨테이너
- 제어의 역전 IoC (Inversion of Control)
- 개념
- 프로그램의 제어 흐름을 직접 제어하는 것이 아닌 외부에서 관리하는 것 - AppConfig 등장 이전
- 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성, 연결, 실행 ⇒ 구현 객체가 프로그램 제어 프름 스스로 조종 - AppConfig 등장 이후
- 구현 객체는 자신의 로직을 실행하는 역할만 담당 → AppConfig가 프로그램에 대한 제어 흐름에 대한 권한 가짐
EX) serviceImpl은 인터페이스를 호출하지만 어떤 구현 객체들이 실행될지 모름
- 프로그램의 제어 흐름을 직접 제어하는 것이 아닌 외부에서 관리하는 것 = 제어의 역전(IoC)
- 개념
- 프레임 워크 VS 라이브러리
- 프레임 워크 : 개발자가 작성한 코드를 제어하고, 대신 실행
EX) JUnit : 자신만의 라이프 사이클을 가지는 프레임 워크가 개발자 코드를 적절한 타이밍에 콜백하는 제어권을 넘기는 것 = IoC
- 라이브러리 : 개발자가 코드를 직접 제어 - 의존관계 주입 DI (Dependency Injection)
- 개념
- 정적인 클래스 의존 관계와, 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계를 분리하여 생각해야 함 - 정적인 의존 관계
- 클래스가 사용하는 import 코드로 의존관계 파악 → 애플리케이션을 실행하지 않아도 분석 가능 - 동적인 객체 인스턴스 의존 관계
- 애플리케이션 실행 시점에서 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계
- 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 의존관계 연결 = 의존관계 주입
- 객체 인스턴스 생성하고 그 참조값 전달해서 연결
- 의존관계 주입 사용 → 클라이언트 코드 변경 X, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경
⇒ 의존관계 주입 사용 → 정적인 클래스 의존관계 변경 X, 동적인 객체 인스턴스 의존관계 변경 O
EX) OrderServiceImpl의 관계 변화 없이(코드의 변화 없이) 동적인 의존관계 가능
- 개념
- DI 컨테이너 = IoC 컨테이너 = 어샘블러, 오브젝트 팩토리 ...
- 정의
- AppConfig 처럼 객체 생성, 관리하며 의존 관계 연결해 주는 것
- 정의
8. 스프링으로 전환
- 1~7까지 순수한 자바 코드로 DI 적용 → SPRING 고고!!
- AppConfig 스프링 기반으로 변경
- @Configuration : AppConfig에 설정 구성, 설정 정보 담당
- @Bean : 스프링 컨테이너에 스프링 빈으로 등록, 각 메서드에 붙임
- 스프링 빈 : 스프링 컨테이너에 등록된 객체
@Configuration // 설정 정보(구성정보) 담당
public class AppConfig { // 애플리케이션의 전체를 설정하고 구성
@Bean // 스프링 컨테이너에 등록
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemoryMemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy(){
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
- 스프링 컨테이너 적용
- ApplicationContext = 스프링 컨테이너
- applicationContext.getBean() : 스프링 컨테이너를 통해 필요한 스프링 빈(객체) 찾아 사용
- 스프링 빈은 @Bean이 붙은 메서드 명 = 스프링 빈 이름
@Bean(name=)를 통해 이름 변경 가능
public class OrderApp {
public static void main(String[] args) {
/* AppConfig 적용 전 */
// MemberService memberService = new MemberServiceImpl();
/* 스프링 적용 전 */
// 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);
Long memberId = 1L;
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
Member findMember = memberService.findMember(1L);
System.out.println("new member = " + member.getName());
System.out.println("findMember = " + findMember.getName());
Order order = orderService.createOrder(memberId, "itemA", 20000);
System.out.println("order = " + order);
}
}
[ 출처 ]
'Spring > 개념' 카테고리의 다른 글
[스프링기본] CH02. 스프링 핵심 원리 이해1 - 예제 만들기 (0) | 2021.08.18 |
---|---|
[스프링기본] CH01. 객체지향설계와 스프링 (0) | 2021.08.16 |
[스프링입문] CH07. AOP (0) | 2021.08.13 |
[스프링입문] CH06. 스프링 DB 접근 기술 (0) | 2021.08.13 |
[스프링입문] CH05. 회원 관리 예제 - 웹MVC 개발 (0) | 2021.08.10 |
댓글