본문 바로가기

스프링/기본편

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

728x90

새로운 할인 정책 개발


 

- 역할과 구현을 분리해서 만들었었음 - 객체지향 원칙 준수 했는가?

 

RateDiscountPolicy 추가 - 정률할인

 

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

 

여기서 ctrl + shift + t 하면 자동으로 테스트 파일 만들어줌

 

RateDiscountPolicyTest

class RateDiscountPolicyTest {

    RateDiscountPolicy discountPolicy = new RateDiscountPolicy();

    @Test
    @DisplayName("Vip는 10% 할인이 적용되어야 한다.")
    void vip_o(){
        //given
        Member member = new Member(1L, "memberVip", Grade.Vip);

        //when
        int discount = discountPolicy.discount(member, 10000);

        //then
        Assertions.assertThat(discount).isEqualTo(1000);
    }

    @Test
    @DisplayName("Vip가 아니면 할인이 적용되지 않아야 한다.")
    void vip_x(){
        //given
        Member member = new Member(2L, "memberBasic", Grade.Basic);

        //when
        int discount = discountPolicy.discount(member, 10000);

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

 

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


//주문 서비스 구현체
public class OrderServiceImpl implements OrderService{
    //2개가 필요
    //리포지토리에서 회원 찾아야하기 때문에 필요
    private final MemberRepository memberRepository = new MemoryMemberRepository();
    //할인 정책 필요 - 정액할인 정책으로
//    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    //할인 정책 변경 - 정률할인으로
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();

주문 서비스 구현체에서 할인 정책 변경했으나 문제점이 있는데, 클라이언트 코드를 변경했다는 것이다.

 

 

왜 클라이언트 코드를 변경해야 했을까

 

 

이렇게 DIP를 위반하면서 할인 정책을 변경할 때도 문제가 생기게 된다.

 

 

문제의 해결법

 

private DiscountPolicy discountPolicy;

이렇게 변경해준다는 소리다. 이렇게 하면 인터페이스에만 의존하게 되는 것은 맞지만 구현체가 없기 때문에 실행해보면 에러가 난다.

 

이를 해결하기 위해서는 누군가가 클라이언트인 OrderServiceImpl 에 DiscountPolicy 의 구현 객체를 대신 생성하고 주입해주어야 한다.

 

DIP는 지켜진거임 여기서 - 안돌아가는게 문제일뿐...

 

관심사의 분리


 

AppConfig의 등장

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

 

public class MemberServiceImpl implements MemberService{

    //가입하고, 조회하기 위해선 저장소가 필요함
    private final MemberRepository memberRepository;
    
    //MemberRepository의 구현체가 뭐가 들어갈지를 생성자를 통해서 결정
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    //이제 MemberServiceImpl은 MemoryMemberRepository에 대해서는 모르고,
    //인터페이스에만 의존하게 되었다.
    //그리고 MemoryMemberRepository는 AppConfig에서 할당되어 MemberRepository에 들어간다.

    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.finById(memberId);
    }
}

 

MemberServiceImpl을 위와 같이 변경하고

 

//애플리케이션 전체를 설정하고 구성한다
public class AppConfig {

    //원래는 객체 생성, 인터페이스에 대한 할당을
    //MemberServiceImpl 여기서 직접 했었음
    //이제 이러한 작업들은 AppConfig에서 대신함
    public MemberService memberService(){
        //생성자 주입
        //MemberServiceImpl을 만들고 여기에 MemoryMemberRepository()를 주입해서 사용할 것이다.
        return new MemberServiceImpl(new MemoryMemberRepository());
    }

    public OrderService orderService(){
        //각각의 인터페이스에 구현체 할당
        //OrderServiceImpl을 만들고 여기에 MemoryMemberRepository()와 FixDiscountPolicy()를 주입해서 사용할 것이다.
        return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
    }
}

 

이렇게 AppConfig 생성

 

 

 

클래스 다이어그램

 

회원 객체 인스턴스 다이어그램

 

//주문 서비스 구현체
public class OrderServiceImpl implements OrderService{
    //2개가 필요
    //리포지토리에서 회원 찾아야하기 때문에 필요
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    
    //인터페이스만 남기고, 구현체는 생서자를 통해서 주입하도록 설정
    //인터페이스에만 의존
    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.finById(memberId);
        //DiscountPolicy로 그냥 정보를 넘겨서 알아서 처리하고 결과를 던지도록 설정
        //회원 정보와 아이템 가격 던져서 FixDiscountPolicy 여기서 계산결과 넘김
        int discountPrice = discountPolicy.discount(member, itemPrice);

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

 

OrderServiceImpl도 위와 같이 변경해준다.

 

 

AppConfig 실행

public class MemberServiceTest {

    MemberService memberService;
    
    //테스트를 실행하기 전에 AppConfig를 만들고
    //memberService에 할당
    @BeforeEach
    public void beforeEach(){
        AppConfig appConfig = new AppConfig();
        memberService = appConfig.memberService();
    }

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

        //when
        memberService.join(member);
        Member findMember = memberService.findMember(1L);

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

 

public class OrderServiceTest {

    MemberService memberService;
    OrderService orderService;

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

    @Test
    void createOrder(){
        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.Vip);
        memberService.join(member);

        Order order = orderService.createOrder(memberId, "itemtA", 10000);
        //할인 금액이 1000원과 같은지 비교
        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
    }
}

 

두 테스트 파일을 위와 같이 변경하고 테스트를 실행하면 된다.

 

정리

 

AppConfig 리팩터링


현재 AppConfig를 보면 중복이 있고, 역할에 따른 구현이 잘 안보인다.

 

기대하는 그림

 

역할과 그에 따른 구현이 한 눈에 보이도록 하는 것이 중요함

 

public class AppConfig {

    public MemberService memberService(){
        return new MemberServiceImpl(new MemoryMemberRepository());
    }

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

하지만 현재에는 그러한 모습이 한 눈에 보이지 않는다.

 

public class AppConfig {

    /*ctrl+alt+m으로 extract method 기능 사용*/
    //각 인터페이스에 대한 역할이 다 드러나는 것을 확인할 수 있음
    //또한 각 인터페이스에 대해서 어떤 구현체를 사용할 것인지를 확인할 수 있음

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

    private MemoryMemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    public OrderService orderService(){
        //설정해둔 메서드 가져와서 생성자에 넣어줌
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

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

 

 

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


 

AppConfig의 생성으로 사용 영역과 구성 영역이 완전히 분리되었고, 할인 정책을 바꾸기 위해서 사용 영역을 바꿀 필요는 전혀 없고 구성 영역의 AppConfig만 수정해주면 된다.

 

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

 

이렇게만 변경해주면 쉽게 변경이 가능하다.

 

 

전체 정리

 

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


여기서는  SRP, DIP, OCP 3가지가 적용되었다.

 

 

IoC, DI 그리고 컨테이너


IoC 제어의 역전

내가 호출하는 것이 아니라 프레임워크가 내 코드를 대신 호출해 주는 것 - 제어권이 뒤바뀐다.

 

 

DI 의존관계 주입

 

 

 

이런 클래스 다이어그램으로는 어떤 객체가 주입되는지 확인 못함

 

 

의존관계 주입으로 위의 클래스 다이어그램을 전혀 바꾸지 않고 의존관계를 변경했다.

 

IoC 컨테이너, DI 컨테이너

 

즉 AppConfig 처럼 의존관계 역전을 시키는 것을 뜻함

 

스프링으로 전환


위의 모든 것은 순수한 자바 코드로만 만들어졌고, 이제 위의 내용을 스프링으로 전환하도록 하겠다.

 

AppConfig 스프링 기반으로 변경

 

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

 

AppConfig에 설정을 구성한다는 뜻의 @Configuration 을 붙여준다.
각 메서드에 @Bean 을 붙여준다. 이렇게 하면 스프링 컨테이너에 스프링 빈으로 등록한다.

 

MemberApp에 스프링 컨테이너 적용

public class MemberApp {
    public static void main(String[] args) {
/* 이 주석의 내용을 아래에서 스프링을 사용해서 실행*/
//        AppConfig appConfig = new AppConfig();
//        MemberService memberService = appConfig.memberService();

        //스프링은 모든것이 ApplicationContext 시작 - 스프링 컨테이너라고 보면 됨
        //@Bean이 붙은 모든 객체들을 관리해줌
        //AppConfig에 있는 환경설정 정보를 가지고 스프링이 @Bean으로 객체 생성한 것을 컨테이너에 넣어서 관리
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        //기본적으로 메서드 이름으로 빈에 등록되기 때문에 원하는 빈을 꺼내오기 위해서는 그 메서드 이름으로 호출해야 함
        //(빈 이름, 타입)
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);

        Member member = new Member(1L, "memberA", Grade.Vip);
        memberService.join(member);

        Member findMember = memberService.findMember(1L);
        System.out.println("new member = " + member.getName());
        System.out.println("find Member = " + findMember.getName());
    }
}

 

OrderApp에 스프링 컨테이너 적용

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

        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.Vip);
        memberService.join(member);

        Order order = orderService.createOrder(memberId, "itemA", 10000);

        System.out.println("order = " + order);
    }
}

 

위와 같이 변경하면 주석처리한 부분으로 실행했을 때와 동일한 결과를 출력한다.

스프링 컨테이너

ApplicationContext 를 스프링 컨테이너라 한다.

 

 

.getBean의 파라미터 값으로 입력한 이름의 빈을 가져오고, 그 타입을 설정해 주면 가져옴

728x90

'스프링 > 기본편' 카테고리의 다른 글

컴포넌트 스캔  (0) 2022.05.22
싱글톤 컨테이너  (0) 2022.05.22
스프링 컨테이너와 스프링 빈  (0) 2022.05.22
스프링 핵심 원리 이해1 - 예제 만들기  (0) 2022.05.21
객체 지향 설계와 스프링  (0) 2022.05.21