[스프링 핵심원리 - 기본편] 섹션 3

‎SE-OL·2022년 5월 24일
0

새로운 할인 정책

왜? -> 섹션 2에서 발견한 DIP, OCP를 못 지키는 문제 해결하기 위해
급작스러운 요구사항이 발생되었을 때
저번 편에서 만든 정책에서 인터페이스 추가!

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코드

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); //alt+enter 온디멘드 ~>. static import


    }

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

문제점

설정 적용을 위해 OrderServiceImpl.java에서 new RateDiscoutPolicy()로 변경 => 여기서 객체를 변경하면서 OCP, DIP 위반 ...
왜? -> OrderServiceImple은 추상 의존(DicountPolicy)뿐 아니라 구체클래스에도 의존...(FixDiscountPolicy, RateDiscountPolicy)

DIP위반 !! -> 인터페이스에만 의존하도록 코드를 변경해줘야함 . .```
public class OrderServiceImpl implements OrderService {
//private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private DiscountPolicy discountPolicy;
} //이제 널포인터에러 발생ㅋ -> 구현 객체 대신 생성 및 주입 필요

AppConfig Refactoring

관심사 분리

new ~~()가 결국은 하나하나 .. 직접해야하는? 코드임 말 그대로. 개빡셈. 집중이 불가능하므로 역할 분담 필요 !!

AppConfig

객체 생성 및 연결하는 클래스
MemeberServiceImpl.java를 다음과 같이 수정

private final MemberRepository memberRepository ;

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

OrderServiceImple.java

private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

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

AppConfig.java

public class AppConfig {
    public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository()); //직접 할당하지 않도록 하는 부분 !
    }
    public OrderService orderService() {
        return new OrderServiceImpl(
                new MemoryMemberRepository(),new FixDiscountPolicy()); // 순서 중요
    }
}

=> 생성자 주입 ~~

굿보이 AppConfig~.

다시 정리
AppConfig 객체는 각 객체를 생성하고 그 참조값을 Impl을 생성하면서 생성자로 전달함. -> Impl의 입장에서 DI라고 함.(의존관계 주입)

MemberApp.java

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl; //사용되지 않음 !!

public class MemberApp {
    public static void main(String[] args) {
        AppConfig appConfig= new AppConfig();
        MemberService memberService = appConfig.memberService();
        //밑은 이전 코드
        //MemberService memberService = new MemberServiceImpl(memberRepository);
        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.java

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;

public class OrderApp {
    public static void main(String[] args) {

        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();
        OrderService orderService = appConfig.orderService();
        //MemberService memberService = new MemberServiceImpl(memberRepository);
        //OrderService orderService = new OrderServiceImpl(memberRepository, discountPolicy);
        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);
    }
}

MemberServiceTest.java

class MemberServiceTest {
    MemberService 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);
    }
}

OrderServiceTest.java

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, "itemA", 10000);
        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
    }
}

-> new 객체 생성은 AppConfig에서만 실행하고 고로 다른 import도 필요 없음 .. 다른 클래스는 그냥 실행만 하고 객체는 AppConfig가 주입해줌.

AppConfig Refactoring

원래의 AppConfig.java 코드 수정

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());
        //return new MemberServiceImpl(new MemoryMemberRepository()); //직접 할당하지 않도록 하는 부분 !
    }
    public OrderService orderService() {
        //return new OrderServiceImpl(new MemoryMemberRepository(),new FixDiscountPolicy()); // 순서 중요
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
    public DiscountPolicy discountPolicy() {
        return new FixDiscountPolicy();
    }
}

new MemoryMemberRepository()의 붕복 부분이 제거되어 AppConfig 부분만 변경하면 된다 !

새 구조와 할인 정책 적용

AppConfig.java

public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();//원래 FixDiscountPolicy()였음
    }


드디어 새로운 할인 정책이 손쉽게 적용 !
DIP위반 문제를 역할 분담을 통해 AppConfig가 의존관계 주입. 또 Refactoring을 통해 중복을 제거하고 역할 명확하게 분리하며 OCP도 완벽하게 지킴 ㅋ

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

SRP

한 클래스는 하나의 책임만 가져야 한다.
AppConfig의 등장으로 구현 객체의 생성과 연결의 책임 분리
클라이언트 객체는 실행만 !

DIP 의존관계

구체화 말고 추상화에 의존해라.
이것 또한 AppConfig가 의존관계 주입하며 해결

OCP

확장에는 오픈, 변경에는 클로즈
AppConfig가 객체 인스턴트를 대신 생성하고 주입하면서 클라이언트 코드는 변경하지 않아도 됨 -> 만족

IoC, DI, 컨테이너

IoC = Inversion of Control

제어의 역전. 자신이 호출 X, 대신 호출해주는 것.
-> AppConfig, 객체는 실행 역할만 담당하게 되면서 제어권이 사라짐
-> Impl또한 로직만 실행하게 됨

<프레임워크 VS 라이브러리
내가 작성한 코드 제어 및 실행(ex.JUnit) VS 코드가 직접 제어 흐름 담당>

의존관계 주입 = Dependency Injection

의존관계 -> 정적인 클래스 의존 관계 + 실행 시점의 동적인 객체 의존 관계
정적인 클래스 의존관계
클래스가 사용하는 import 코드로 쉽게 파악 가능.
코드 수정 전 기존 코드
동적인 객체 인스턴스 의존관계
런 타임에 외부에서 객체를 생성하고 연결해주어 클라이언트와 서버의 실제 의존관계가 연결이 되는 것
AppConfig 생성 이후의 코드

IoC 컨테이너, DI 컨테이너

-> AppConfig와 같이 객체를 생성 및 관리, 의존관계 연결해줌
<의존관계 주입에 초첨 = 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 설정 구성
@Configuration
public class AppConfig {
    //스프링 빈
    @Bean
    MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
        //return new MemberServiceImpl(new MemoryMemberRepository()); //직접 할당하지 않도록 하는 부분 !
    }
    @Bean
    public OrderService orderService() {
        //return new OrderServiceImpl(new MemoryMemberRepository(),new FixDiscountPolicy()); // 순서 중요
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
    @Bean
    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }
}

MemberApp에서 원래 AppConfig코드 대신

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);

로 수정
OrderApp 또한 마찬가지

ApplicationContext applicationContext = new
AnnotationConfigApplicationContext(AppConfig.class);
 MemberService memberService =
applicationContext.getBean("memberService", MemberService.class);
 OrderService orderService = applicationContext.getBean("orderService",
OrderService.class);

여기서 ApplicationContext는 아까 설명했던 컨테이너.
스프링 사용 전에는 개발자가 직접 AppConfig 통해서 직접 객체 생성 및 의존관계 주입을 했다면 이제는 컨테이너를 대신 돌린다.

  • @Configuration을 설정정보로, @Bean을 통해서 스프링 컨테이너에 메소드를 등록 (스프링 빈의 이름은 메소드 명으로)
  • 객체 조회는 applicationContext.getBean()을 통해서 가능

//아직은 원리까지 .. 장점은 차차 알아가봅시다!

0개의 댓글