스프링 핵심 원리 이해2 - 객체지향원리 적용

목차

  • 새로운 할인 정책 개발
  • 새로운 할인 정책 적용과 문제점
  • 관심사의 분리
  • AppConfig 리팩터링
  • 새로운 구조와 할인 정책 적용
  • 전체 흐름 정리
  • 좋은 객체 지향 설계의 5가지 원칙의 적용
  • IoC, DI, 그리고 컨테이너
  • 스프링으로 전환하기

핵심은 객체지향 원리 적용하기





새로운 할인 정책 개발

정률 할인 정책 개발

package hello.core.discount;

import hello.core.member.Grade;
import hello.core.member.Member;

public class RateDiscountPolicy implements DiscountPolicy{

    int discountPercent = 10; //10프로 할인

    @Override
    public int discount(Member member, int price) { //컨트롤 쉬프트 티 =테스트 클래스 생성 단축키

        if (member.getGrade() == Grade.VIP){
            return price*discountPercent/100;  //vip이면 10할인
        }
            else{
         return  0;
        }
    }
}




새로운 할인 정책 적용과 문제점

새로운 정책 할인 적용 및 테스트

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberMemoryRepository;
import hello.core.member.MemberRepository;

public class OrderServiceImpl implements OrderService{

    MemberRepository memberRepository = new MemberMemoryRepository();

    //DiscountPolicy discountPolicy = new RateDiscountPolicy(); 변경 전 
    DiscountPolicy discountPolicy = new RateDiscountPolicy();  //변경 후

    @Override
    public Order orderCreate(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
        int discountPrice = discountPolicy.discount(member, itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);


    }

}

테스트

package hello.core.discount;

import hello.core.member.Grade;
import hello.core.member.Member;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;


class RateDiscountPolicyTest {

    RateDiscountPolicy rateDiscountPolicy = new RateDiscountPolicy();

    @Test
    @DisplayName("vip 맞을시") //테스트함수명대신 나오는 스트링
    public void 퍼센트할인테스트() {

        Member member = new Member(1L, "승빈", Grade.VIP);  //새로운 멤버 생성

        int discount = rateDiscountPolicy.discount(member, 10000); 

        Assertions.assertThat(discount).isEqualTo(1000); //할인 가격이 1000이 맞는지 확인 
    }

    //성공 테스트도 중요하지만 실패 테스트도 꼭 만들어 봐야한다.

    @Test
    @DisplayName("vip 아닐시")
    public void 퍼센트할인테스트2() {

        Member member = new Member(1L, "승빈", Grade.BASIC);

        int discount = rateDiscountPolicy.discount(member, 10000);

        Assertions.assertThat(discount).isEqualTo(0);
    }
}

새로운정책 테스트

문제점

    //DiscountPolicy discountPolicy = new RateDiscountPolicy(); 변경 전 
    DiscountPolicy discountPolicy = new RateDiscountPolicy();  //변경 후
  • 새로운 정채을 변경할때 새로운 정책의 구현채만 바꾸어주면 되지만 여기에는 아래와 같은 문제점이 있다
    • OCP,DIP 객체지향 설계 원칙 위반
      • DIP : OrderServiceImpl클래스는 DiscountPolicy 인터페이스에 의존하면서 DIP를 지킨거같지만 사실은 인터페이스 뿐만아니라 구현 클래스에도 의존하고 있다.
      • OCP: 지금 코드는 기능을 확장해서 변경하려면, 클라이언트 코드에 영향을 준다.

해결 방안

이 문제를 해결하기위해선 누군가가 클라이언트인 OrderService에 DiscountPolicy이 구현 객체를 대신 생성하고 주입해주어야 한다.
(AppConfig라는 애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고 , 연결하는 책임을 가지는 별도의 설정 클래스를 만들자!)




관심사의 분리

애플리케이션의 전체 동작 방식을 구성(config) 하기위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스를 만들자.

AppConfig

package hello.core;


import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberMemoryRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImp;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

//인텔리제이 단축키 : 컨트롤 e 과거 히스토리나와서 빠르게 화면 전환가능

/*애플리케이션의 전체 동작 방식을 구성(config)하기위해, config 클래스에서 구현객체를 구성하고, 연결하는 책임을
가지는 별도의 설정 클래스를 만든다.*/
/*이전에는 인터페이스의 구현객체가 필요한기능의 구현객체를 직접 new해서 생성했지만,  이제는 AppConfig
클래스가 동작에 필요한 구현객체를 생성한다.*/
//그리고 AppConfig는 생성한 객체 인스턴스의 참조(래퍼런스)를 생성자를 통해서 주입(연결)해준다.
public class AppConfig {

    public MemberService memberService(){
        return new MemberServiceImp(new MemberMemoryRepository());
    }

    public OrderService orderService(){
        return  new OrderServiceImpl(new MemberMemoryRepository(),new FixDiscountPolicy());
    }
}

MemberServiceImpl

package hello.core.member;

public class MemberServiceImp implements MemberService {

    private final MemberRepository memberRepository;

    public MemberServiceImp(MemberRepository memberRepository){
        this.memberRepository = memberRepository;
    }
    // 생성자 주입을 통해서 MemberRepository에 어떤 객체가 들어갈지 결정한다. -> 의존관계에 대한 고민은 외부에서 결정 ->실행에만 집중
        // DIP 완성

    @Override
    public void 회원가입(Member member) {
        memberRepository.save(member);
    }

    @Override
    public Member 회원조회(Long memberId) {
       return memberRepository.findById(memberId);
    }

}

OrderServiceImpl

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberMemoryRepository;
import hello.core.member.MemberRepository;

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;

    }

    @Override
    public Order orderCreate(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
        int discountPrice = discountPolicy.discount(member, itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);


    }

}

구현체에서 필요한 구현체(기능)들을 AppConfig에서 정해진 구현체를 파라미터로 받아 각각의 인터페이스의 구현체를 할당받았다. 따라서 MemberServiceImpl,OrderServiceImpl은 필요 구현체의 인터페이스만 의존함으로 해당 구현체의 로직에만 집중 할 수 있다. 또 DIP원칙을 지킬 수 있게 된다.



테스트 코드

MemberServiceTest

package hello.core.member;

import hello.core.AppConfig;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;

public class MemberServiceTest {



MemberService memberService;

@BeforeEach  
 public void beforeEach(){
    AppConfig appConfig = new AppConfig();
    memberService = appConfig.memberService();

}

    @Test
    void join(){
        //given
        Member member = new Member(1L,"승빈",Grade.VIP);

        //when
        memberService.회원가입(member);
        Member member1 = memberService.회원조회(1L);

        //then
        Assertions.assertThat(member).isEqualTo(member1);
    }
}

테스트코드에서 @BeforeEach 는 각테스트 코드가 실행되기 전에 실행된다.

  • 테스트코드도 AppConfig에서 memberService()를 호출해 리턴받은 구현체를 memberService에 할당해 테스트한다.OrderServiceTest
  • package hello.core.order;

import hello.core.AppConfig;
import hello.core.member.*;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class OrderServiceTest {

MemberService memberService;
OrderService orderService;

@BeforeEach
public void beforeEach(){
    AppConfig appConfig = new AppConfig();
    memberService = appConfig.memberService();
    orderService = appConfig.orderService();
}

@Test

void join(){

    //given
    Long memberId = 1L;
    Member member = new Member(memberId, "승빈", Grade.VIP);
    memberService.회원가입(member);

    //when

    Order order = orderService.orderCreate(memberId, "물통", 10000);

    //then
    Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
}

}

- AppConfig에서 memberServic(),orderService()를 호출해 리턴받은 구현체를 memberService,orderService를 할당해 테스트코드에 사용한다.
![Config를 통한 테스트 코드](https://user-images.githubusercontent.com/89888075/163774417-f4327bb2-9a9b-4540-8114-b958741b4dff.PNG)

### 정리
- AppConfig를 통해서 관심서를 확실하게 분리했다.
- 각각의 Impl은  기능을 실행하는 책임만 지면 된다.

<br>
<br>
<br>

# AppConfig 리팩터링

### 리팩터링 전
~~~java
package hello.core;


import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberMemoryRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImp;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class AppConfig {

    public MemberService memberService(){
        return new MemberServiceImp(new MemberMemoryRepository());
    }

    public OrderService orderService(){
        return  new OrderServiceImpl(new MemberMemoryRepository(),new FixDiscountPolicy());
    }
}
  • 리팩터링 전은 전체 구성의 역할이 명확하게 구분이 힘들다.리팩터링 후
  • package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberMemoryRepository;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImp;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class AppConfig {

public MemberService memberService(){
    return new MemberServiceImp(memberRepository());
}

public MemberRepository memberRepository(){
    return new MemberMemoryRepository();

}

public DiscountPolicy discountPolicy(){
    return new FixDiscountPolicy();
}


public OrderService orderService(){
    return  new OrderServiceImpl(memberRepository(),discountPolicy());
}

}

//리팩토링 해줌으로써 역활이 명확하게 구별된다.

- 리팩터링을 해줌으로서 전체적인 열할을 구분하기 쉬워 졌다.
-new MemberMemoryRepository() 중복이 해결되며 MemberRepository의  구현체 변경이 필요할 경우 구현체만 MemberRepository의 구현체만 바꾸어 주면 된다.
<br>
<br>
<br>

# 새로운 구조와 할인 정책 적용
- 처음으로 돌아가서 정액 할인 정책을 정률% 할인 정책으로 변경해보자.
~~~java
package hello.core;


import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberMemoryRepository;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImp;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class AppConfig {

    public MemberService memberService(){
        return new MemberServiceImp(memberRepository());
    }

    public MemberRepository memberRepository(){
        return new MemberMemoryRepository();

    }

    public DiscountPolicy discountPolicy(){
       // return new RateDiscountPolicy();
        return new RateDiscountPolicy(); // 정률(새로운)할인정책으로 변경
    }


    public OrderService orderService(){
        return  new OrderServiceImpl(memberRepository(),discountPolicy());
    }


}

사용영역 구성영역 구분

  • AppConfig(구성영역)을 따로 만들어 줌으로 써 기능영역(클라이언트 코드)을 변경할 필요없이 구성영역의 코드만 변경해줌으로써 DIP,OCP원칙을 지키면서 새로운 정책할인으로 변경할 수 있게 되었다.




전체 흐름 정리

  • 새로운 할인 정책 개발
  • 새로운 할인 정책 적용과 문제점
  • 관심서의 분리
  • AppConfig 리팩터링
  • 새로운 구조와 할인 정책 적용

새로운 할인 정책 개발

  • 다형성 덕분에 새로운 정률 할인 정책 코드를 추가로 개발하는 것은 문제가 없었다.

새로운 할인 정책 적용과 문제점

  • 다형성덕에 새로운 정책코드를 추가 개발하는 것에는 문제가 없지만, 이를 적용하려하니 클라이언트 코드인 주문 서비스 구현체도 함께 변경해야해서 (인터페이스와 구현클래스를 함께의존)
    인터페이스만 의존해야하는 DIP법칙을 위반하게 되었다.

관심서의 분리

  • DIP 를 지키기 위해 사용 영역과 구성영역을 나눠 전체 동작 방식을 구성하는 AppConfig클래스를 생성하여 구현객체를 생성하고, 연결하는 책임을 주었다.

AppConfig 리팩터링

  • 리팩터링을 함으로써 구성 정보에서 역할과 구현을 명확하게 분리
  • 역할이 잘 드러난다.
  • 중복이 제거되었다.

새로운 구조와 할인 정책 적용

  • FixDiscountPolicy 에서 RateDiscountPoric로 할인정책 변경
  • 기존에는 Impl클래스에서 필요한 구현객체를 변경하였지만 이제는 구성영역인 AppConfig에서만 변경이 가능해 졌다.

    정리
    • 전체적인 구성영역(AppConfig)를 만듬으로써 DIP,OCP원칙을 지키면서 새로운 정책으로 변경할 수 있게 되었다.




좋은 객체 지향 설계의 5가지 원칙의 적용

지금까지 3가지 SRP,DIP,OCP원칙을 적용해 보았다.



SRP 단일 책임 원칙

한 클래스는 하나의 책임만 가져야 한다.

  • 구현 객체를 생성하고 연결하는 책임은 AppConfig가 담당
  • 클라이언트 객체는 실행하는 책임만 담담

DIP 의존관계 역전 원칙

프로그래머는 "추상화에 의존해야지 , 구체화에 의존하면 안된다." 의존성 주입은 이 원칙을 따르는 방법 중 하나다.

  • AppConfig로 구성영역을 따로 나누기 전은 Impl들이 필요한 기능이 있을떄 인터페이스와 구혀늘래스 둘다 의존 했기때문에 추상화에만 의존해야하는 DIP원칙을 위반 했다.
  • AppConfig가 클라이언트 코드 대신 클라이언트 코드에 의존관계를 주입해 DIP원칙을 따라 문제를 해결했다.

OCP

소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.

  • 구성요소를 따로 담당한는 AppConfig가 있기 때문에 소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀 있다.




IoC, DI, 그리고 컨테이너

IoC(Inversion of Control) 제어의 역전

  • 이전에는 OverServiceImpl에서 직접 필요한 구현객체를 생성했지만 AppConfig를 만든 이후로는 AppConfig가 OrderService인터페이스에 다른 구현객체를 생성하고 실행하여 프로그램의 제어의흐름을 가저간다.
    OverServiceImpl는 로직만 실행한다.
  • 이렇게 프로그램의 제어의 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IOC)라고 한다.

프레임워크 vs 라이브러리

  • 프레임워크 : 내가 작성한 코드를 제어하고, 대신 실행하면 프레임워크이다.(JUnit) = 프레임워크는 자기만의 동작 흐름이있고 개발자는 이 개발흐름에 맞춰 개발한다.
  • ex JUnit에는 @BeforeEach,@Test가 있고
    @Test 가 실행되기전 @BeforeEach가 실행되야한다.
    따라서 개발자는 이 흐름에 맞춰 개발을 해야하며 내가 작성한 코드를 프레임워크가 제어한다.
  • 라이브러리 : 내가 작성한 코드가 직접 제어의 흐름을 담당한다. = 라이브러리는 필요에 따라 라이브러리를 사용해 개발자가 직접 개발흐름을 정할수있다.

DI (Dependency Injection) 의존관계 주입

  • 의존관계란 말그대로 서로 의존하는 관계라는 것이다
    예를들어 A가 B를 의존할때 B가 변하면 A도 영향을 미친다.
  • 의존 관계는 정적인 클래스 의존 관계, 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계 를 분리해서 생각해야한다.

정적인 클래스 의존관계

클래스 다이어그램

클래스 의존관계

  • 정적인 클래스 의존관계는 import 코드만 보고 의존관계를 쉽계 판단할 수 있다.
  • OverserviceImpl은 DiscountPolicy,MemberRepository에 의존한다는 것을 알 수있다.
  • 정적인 클래스 의존관계는 실제로 각 인터페이스에 어떤 객체가 주입 될지는 알 수 없다.

    동적인 객체 인스턴스 의존 관계
  • 객체 다이어그램
    객체 인스턴스 의존관계
  • 객체 다이어그램은 실행 시점에 실제 생성된 객체 인스턴스 참조가 연결된 의존 관계이다.

IoC컨테이너와 DI컨테이너

  • AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 IoC컨테이너 또는 DI컨테이너라고한다.




스프링으로 전환하기