middlemoon

Spring - 스프링 핵심 원리 이해2 (객체 지향 원리 적용) 본문

Develop/Spring

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

중대경 2023. 2. 6. 08:00

이번 시간은 저번에 했던것을 이어 새로운 할인 정책 개발에 관련되 이어나가보려한다. 일련의 과정은 기획자가 개발자에게 요구 및 설계를 제안할때 나오는 과정과 같은데 기획자의 요구사항에 완벽하게 맞출수 있는 개발자는 없다고 생각한다. 물론 완벽에 가까운 요구사항을 맞출수는 있겠지만 그렇지 않은 상황을 대비해 개발자는 유연하게 이 문제들을 대처할수 있도록 객체지향 설계 원칙을 준수하며 짜는 것이

현명하다고 생각한다. 천천히 과정들을 살펴보며 어떻게 해야 원칙을 준수 할수 있는지 확인해보도록 하자.

 

 

 

이 블로그는 김영한님 강의를 토대로 작성하였으니 참고용으로 봐주시면 좋을 것 같고, 기록용으로 남기기 위해 올렸다는 점 참고해주시면 감사하겠습니다.

 

 


 

 

설계도는 다음과 같다.저번시간에 OrderServiceImpl과 DiscountPolicy 안에 Interface로 FixDiscountPolicy 상속한적이 있었다

이렇게 하는 이유는 고정금리와 변동금리가 존재하기 때문에 각 클래스에 해당하는 역할과 구현을 나타내기 위해 나눈다고 설명을 하였었다

오늘은 RateDiscountPolicy에 관한 코드를 작성할 계획이다.

 

 

 

 

RateDiscountPolicy.java

 

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

 

간단한 비즈니스로직 구조이다. 코드를 짜다보면 Rate와 관련된 %(수치)를 나타내는 기능을 구현해야하는 경우가 있는데,

discountPercent를 인스턴스 변수로 선언해준 이유는 변동되는 금리이기 때문에 수치화를 나타내주었다.

꿀팁인데, 코드를 짜고 테스트코드를 만들때 command + shift + t 를 누르게 되면 테스트코드를 짤수 있게끔 클래스가 만들어진다

 

테스트 코드 또한 강조되는 바는 다음과 같다.

1.테스트 코드의 목적은 분명하게 짜둘것

2.실패로 나오는 테스트 코드를 여러개 생각해서 짤것

 

RateDiscountPolicyTest.java

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.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
        Assertions.assertThat(discount).isEqualTo(1000);

    }

    @Test
    @DisplayName("VIP가 아니면 할인이 적용되지 않아야 한다")
    void vip_x(){
        //given
        Member member = new Member(1L, "memberBASIC", Grade.BASIC);
        //when
        int discount = discountPolicy.discount(member, 10000);
        //then
        Assertions.assertThat(discount).isEqualTo(1000);

    }

}

 

 

테스트 코드 성공

 

테스트 코드 실패

 

 

 

자, 여기까지는 새롭게 들어오는 할인정책에 대해서 적용을 해보았고 객체지향의 원리에 대해서 한번이라도 생각해봤다면

몇가지 원칙에 위배되는 사실을 확인하게 되는데

 

 

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(); //변수명 중복

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

 

우리의 의존관계는 OrderServiceImpl 클래스가 FixDiscountPolicy()를 바라보고 있다는 사실이다.

이 관계를 우리는 DIP가 위반되었다. 라는 점을 인식할수 있게 된다

 

그렇게 되면 우리는 인터페이스에만 의존할 수 있도록 나타내야 할텐데, DiscountPoliy라는 인터페이스만 바라볼수 있도록 만들어줘야한다. 그래서 객체로 만든 FIxDiscountPoliy(), RateDiscountPolicy()를 만들어놓는것이 아니라 DiscountPolicy만 만들어 놓게 해야한다. 여기까지 좋다. 그런데 문제가 있게 되는데 저런식으로 돌리게 된다면 Fix,Rate에 비즈니스로직이 있기 마련인데

discountPolicy에는 아무것도 없으므로 Null값이 분명 찍히게 된다. DIP원칙이 위배되서 바꿔줬더니 Null값이..?

 

이제 이런 부분들을 해결해보고자 한다.

 

단일 책임원칙, DIP원칙을 지키면서 짜보고자한다.

우선 새롭게 AppConfig라는 파일을 만드는데, 구현객체를 만들어주고,연결하는 별도의 클래스이다. 쉽게말하자면 Main메소드 정도될수있을 것 같다. 코드는 다음과 같다

 

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

}

 

코드만 보면 단순하다 생각할 수 있는데, 

 

//    private final MemberRepository memberRepository = new MemoryMemberRepository();   //DIP위배코드
    private final MemberRepository memberRepository;    //DIP원칙 O 밑에 생성자를 따로 만들어 AppConfig안에 MemoryMember생성자 O

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

처음 주석처리된곳이 MemberRepository 와 MemoryMemberRepository 이다. 지금은 간단해서 얽혀도 문제가 없겠지만

memberRepository와 MemoryMemberRepository 랑은 별개의 역할을 하자 ! 라는 의미로 쉽게 생각하면 좋을것 같다.

 

그리하여 MemberRepository안에 있는 것을 생성자처리하여 위의 AppConfig 클래스안에 리턴값에 객체를 하나만들어준다는 의미이다. 이것은 DI(Dependency Injection)이라고도 하며 생성자 주입정도 말할수 있을 것 같다.

 

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;        //인터페이스만 철저하게 의존한다.

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

 

OrderServiceImpl와 같은 코드도 마찬가지로 주석처리 된 다양한 객체는 다른곳을 바라보고 있기 때문에 클래스가 너무 많은 역할을 하게된다. 그리하여 인터페이스만 철저하게 의존할수 있도록 각각 선언 후 생성자로 만들어주어 하나의 책임만 만들어줄 수 있는 클래스를 만들어 주면 된다는 말이다 !!

 

 

 

 

AppConfig before refactoring 

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

}

 

AppConfig after refactoring 

package hello.core;

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

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


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

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

}

 

아래 코드는 Appconfig에서 return값 FixDiscountPolicy or  RateDiscountPolicy 둘중 변경했을 때

간단한 수정을 거쳐 결과값이 다르게 나온것을 확인할 수 있다.

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

//        return new RateDiscountPolicy();
    }


result : order = Order{memberId=1, itemName='itemA', itemPrice=20000, discountPrice=1000}
order.calculatePrice = 19000

Process finished with exit code 0


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

        return new RateDiscountPolicy();
    }



order = Order{memberId=1, itemName='itemA', itemPrice=20000, discountPrice=2000}
order.calculatePrice = 18000

Process finished with exit code 0

 

 

좋은 객체지향 설계의  조건들

 

SRP 단일 책임 원칙
한 클래스는 하나의 책임만 가져야 한다.


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


OCP

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


제어의 역전 IoC(Inversion of Control)


의존관계 주입 DI(Dependency Injection)

 

public class AppConfig {

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

        return new RateDiscountPolicy();
    }

}

 

Appconfig 처럼 객체를 생성하고 관리하며 의존관계를 연결해주는 것을 Ioc Container, DI conatiner라고 말할수 있다.

 

Java 코드를 Spring 환경으로 변화

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

}

 

1. Appconfig파일에 어노테이션 @Configuration -> 해당 메서드에 @Bean 각각추가

 

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);
//        MemberService memberService = new MemberServiceImpl();
        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());

    }
}

2. MemberApp 클래스에 해당하는 경로로 들어가 Spring을 생성하는 ApplicationContext 객체생성(AnnotationConfigApplicationContext)

 

3. 찾고자 하는 객체로 들어가 아래 MemberService 클래스 선언해준 것 처럼 동일하게 선언

 

 

Comments