본문 바로가기
Spring/개념

[스프링기본] CH03. 스프링 핵심 원리 이해2 - 객체 지향 원리 적용

by MINNI_ 2021. 8. 31.

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);
    }
}

[ 출처 ]

 

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., 스프링 핵심 원리를 이해하고, 성장하는 개발자가 되어보세요! 📣 확인해주

www.inflearn.com

댓글