JAVA & SPRING/Spring 핵심원리-기본

스프링 핵심 원리-3일차(AppConfig로 OCP, DIP 원칙 준수)

눈오는1월 2023. 7. 26. 16:52
728x90

이제 순수자바로 개발했을때 문제점과 해결방안 그리고 이것을 스프링으로 변환하는 과정을 할것이다.

이제 기획자가 할인을 1000원이 아닌 10% 할인으로 바꿨다고 한다. 할인정책을 바꾸는 과정을 진행해보자

discount 패키지에 RateDiscountPolicy 라는 클래스를 만든다.

package hello.core.discount;

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

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>discount에 RaetDiscountPolicyTest코드를 작성한다.

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() {
        //given
        Member member = new Member(1L, "memberVIP", Grade.VIP);

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

        //then
        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
        assertThat(discount).isEqualTo(0);

    }

}

위 코드처럼 @displayName 어노테이션을 통해 테스트의 이름을 설정할 수 있다.

테스트 성공

테스트를 성공시켰으니 이 할인정책을 적용시켜야한다. OrderServicceImpl에 코드를 고쳐야한다.

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.MemberRepository;
import hello.core.member.MemoryMemberRepository;

public class OrderServiceImpl implements OrderService {


    private final MemberRepository memberRepository = new MemoryMemberRepository();  // 회원 찾아야하고
    //private final DiscountPolicy discountPolicy = new FixDiscountPolicy(); // 할인정책 찾아야함
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy(); // final은 값이 무조건 할당되어야함
    @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);
    }
}

여태까지의 개발 과정에서 문제점을 생각해보자

초반에 말했던것처럼 다형성을 잘 지켜야 하고 스프링은 이것을 도와준다했다(물론 아직 스프링부트에서 개발하고 있지만 스프링을 활용하진 않았다)

우리는 인터페이스와 구현 객체를 분리를 통해서 다형성도 활용하고 역할과 구현을 분리했다.

BUT OCP,  DIP 객체지향 설계 원칙을 지켰을까? -> 결국 지키지 못했다.

위 코드인 OrderServiceImpl은 인터페이스에도 의존하면서 구현 클래스에도 의존하고 있다 그렇기에 DIP 원칙을 지키지 못했다.

또한 OCP는 변경하지 않고 확장할 수 있다고 했지만 우리는 OrderServiceImpl를 수정을 변경해야했다. 그렇기에 OCP원칙을지키지 못했다.

 

즉 개발할때 OrderServiceImpl이 DiscountPolicy만 의존하려고 설계를 했지만 구현 객체도 의존했기 때문

인터페이스에만 의존하지 못하고 구현 객체에도 의존하게 됨

그렇기에 우리는 인터페이스에만 의존할 수 있도록 코드를 변경해야한다.

private final DiscountPolicy discountPolicy = new RateDiscountPolicy(); 이부분을 주석처리하고 인터페이스만 의존할 수 있게끔 바꾼다.

private DiscountPolicy discountPolicy; -> 이코드를 추가한다

 

근데 이렇게 됐을때 구현체가 없기에 NPE(null pointer exception)이 발생한다. 그럼 이러한 문제만 해결하면 DIP,OCP 원칙도 지킬 수 있다.

이 문제를 해결하기 위해 AppConfig 라는 클래스를 생성한다(이 클래스는 구현 객체 생성 및 연결하는 역할)

hello.core에 AppConfig 클래스를 만든다.

//AppConfig 클래스
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;


public class AppConfig {

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

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

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

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

이렇게 한후 memberServiceImpl에 생성자가 없기에 생성자를 만든다.

//MemberServiceImpl class
package hello.core.member;

public class MemberServiceImpl implements MemberService{


    //private final MemberRepository memberRepository = new MemoryMemberRepository();
    // 인터페이스를 의존하고 할당하는부분이 구현체를 의존한다. DIP 위반하고 있는거여서 변경할때 어려움이 있다.
    private MemberRepository memberRepository;

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

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

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

마찬가지로 OrderServiceImpl에도 생성자를 만든다.

//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.MemberRepository;
import hello.core.member.MemoryMemberRepository;

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

    //    private final MemberRepository memberRepository = new MemoryMemberRepository();  // 회원 찾아야하고
//    private final DiscountPolicy discountPolicy = new FixDiscountPolicy(); // 할인정책 찾아야함
    //private final DiscountPolicy discountPolicy = new RateDiscountPolicy(); // final은 값이 무조건 할당되어야함
    //private 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);
    }
}

클래스 다이어그램으로 보여서 설명을 하자면,

클래스 다이어그램

이런식으로 MemberServiceImpl은 MemberRepository만 의존할 수 있게끔 AppConfig가 역할을 수행한다고 보면 된다.

저번에 만들었던 MemberApp과 OrderApp에서 Appconfig가 실행될 수록 수정한다.( 단순하게 객체 실행하고 메소드를 호출하면 되기에 코드는 생략한다)

 

또 테스트코드로 테스트를 돌리기 위해 MemberServiceTest와 OrderService 역시 객체를 직접적으로 생성하지 않고 AppConfig를 활용한다. 이때 @BeforeEach를 활용한다 ( 이 어노테이션은 테스트를이 돌아가기 전마다 실행됨)

참고 * 인텔리제이에서 그 전 코드로 돌가가는 방법은 맥 기준 command + E + 엔터를 치면된다. (command + E를 치면 우리가 무엇을열었는지 history가 나옴)

현재 우리는 이 AppConfig를 통해서 5가지 원칙중 3가지 원칙을 적용했다.

AppConfig를 통해 클라이언트 객체는 구현 객체를 생성하고 연결하는 행동을 안하고 오로지 실행하는 책임만 담당했다.(SRP)

또한 AppConfig를통해서 인터페이스에만 의존할 수 있게 되었다(DIP)

마지막으로 할인 정책을 바꾸더라도 클라이언트 객체의 수정없이 AppConfig만 갈아끼면 되게되었다(OCP)

 

 

더이상 클라이언트 객체가 아닌 프로그램에 대한 제어 흐름이 AppConfig가 가지게 되었는데 이런식으로 프로그램의 제어 흐름을 직접하는것이 아닌 외부에서 관리하는 것을 제어의 역전(IOC) 이라고 한다 ( IOC = Inversion of Control)

 

다른 얘기를 잠깐 하자면 프레임워크 vs 라이브러리의 차이점이 프레임워크는 내가 작성한 코드를 제어하고 대신 실행해주는 녀석들이고 라이브러리는 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 그건 라이브러리다.

 

이처럼 AppConfig 처럼 객체 생성 및 의존관계를 연결해주는 역할을 IOC 컨테이너 or D컨테이너 라고 부름(보통 DI 컨테이너 라고 부른다) 또는 어셈블러, 오브젝트 팩토리 등으로도 불림

 

이제 우리 코드를 스프링으로 전환해보자

AppConfig 클래스를 스프링 기반으로 변경한다.

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

이제 MemberApp과 orderApp도 스프링 컨테이너를 적용해준다

//MemberApp
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(); //AppConfig 적용방법
//        MemberService memberService = appConfig.memberService(); // appConfig에서 다 결졍

        //MemberService memberService = new MemberServiceImpl(); // DIP, OCP 원칙 준수 x

        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
package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.order.Order;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class orderApp {

    public static void main(String[] args) {
        //AppConfig appConfig = new AppConfig();
        //AppConfig 적용
//        MemberService memberService = appConfig.memberService();
//        OrderService orderService = appConfig.orderService();
        // OCP,DIP 원칙 준수 x
//        MemberService memberService = new MemberServiceImpl();
//        OrderService orderService = new OrderServiceImpl();
        // 스프링컨테이너 기반
        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.toString());
        System.out.println("order.calculatePrice = " + order.calculatePrice());
    }
}

뭔가 코드가 더 복잡해졌는데 스프링 컨테이너를 사용하게 되면 어떤 장점이 있을지는 다음 강의시간에~

728x90