WEB/Spring

김영한 (스프링 핵심원리 3) 스프링의 핵심원리이해2 - 객체 지향원리 적용

Tony Lim 2021. 2. 10. 15:49

이미 역할과 구현을 분리해서 설계되었기 때문에 쉽게 정률 discounting을 구현 해주고 적용 시킬 수 있다.

package hello.core.discount;

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

public class RateDiscountPolicy implements DiscountPolicy
{
    private int discountPrecent = 10;

    @Override
    public int discount(Member member, int price)
    {
        if (member.getGrade() == Grade.VIP)
        {
            return price * discountPrecent / 100;
        }
        else
        {
            return 0;
        }
    }
}

VIP인 경우에만 10% 할인을 해주는 것이다.

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;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;

class RateDiscountPolicyTest
{
    RateDiscountPolicy discountPolicy = new RateDiscountPolicy();

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

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

        assertThat(discount).isEqualTo(1000);
    }

    @Test
    @DisplayName("VIP가 아니면 할인이 적용되지 않아야 한다.")
    void vip_x()
    {
        Member member = new Member(1L, "memberVIP", Grade.BASIC);

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

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

그에 상응하는 테스트 코드이다. 잘 적용이 되는것도 확인을 해야하지만 되지 않는지도 확인을 해주어야 한다.

    private final MemberRepository memberRepository = new MemoryMemberRepository();
//    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();

하지만 이제 바뀐 할인정책을 적용할려면 OrderServiceImpl 코드를 수정해주어야 한다. 추상(인터페이스)뿐 만아니라 구체(구현) 클래스에도 의존을 하고 있다.

어떻게 코드를 안고치고 해결 할 수 있을까? 나 말고 누군가 클라이언트인 OrderServiceImpl 에 Discountpolicy의 구체적인 객체를 생성해서 넣어주어야 한다.

 

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

MeberServiceImpl 이나 OrderServiceImpl 은 이제 더이상 구체적인 구현에 의존하지 않는다. Appconfig가 구현 객체를 생성하고 생성자를 통해서 직접 넣어주기 떄문이다.

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy)
    {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
private final MemberRepository memberRepository;

    public MemberServiceImpl(MemberRepository memberRepository)
    {
        this.memberRepository = memberRepository;
    }

이제 두 impl 들은 완전히 interface에만 의존을 하게되는 형식이 된다. 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중 하면 된다. DIP 를 만족 시키는 것이다.

기존의 AppConfig 리팩토링을 통해 좀더 "역할" 들을 들어나게 해보자.

package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberRepository;
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(memberRepository());
    }

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

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

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

memberService 를 쓰기위해서 memberRepository를 받아오는데  memberRepository는 추후에 다른 메모리 방식 말고 다른 구현체로 바뀌어도 memberService는 영향을 주지 않는다. 

orderService를 쓰기위해서 memberRepository와 discountPolicy를 받아오는데 마찬가지로 추후에 다른 구현체로 바뀌어도 orderService는 영향을 전혀 받지 않는다.

역할들이 매우 명확해진것을 확인 할 수 있다.

 

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

사용영역과 구성영역이 명확하게 나뉘어져 있는 것을 확인 할 수 있다. DiscountPolicy의 정책이 바뀌면 구성영역을 변경시켜주면 된다.

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

 

이제 Spring을 이용해서 리팩토링 해본다.

package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
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 MemberRepository memberRepository()
    {
        return new MemoryMemberRepository();
    }

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

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

스프링 컨테이너에 Bean으로 등록을 해준다. 

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
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, "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());
    }
}

@Configuration 이 Appconfig 에 적혀있으니 이를 기반으로 @Bean이라 적힌 모든 메소드를 호출해서 반환되는 객체를 스프링 컨테이너에  Spring Bean으로 등록을 시킨다.