Spring Framework/[인프런] Spring 핵심 원리

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

계란💕 2022. 8. 12. 16:20

3.1 새로운 할인 정책 개발

 

  새로운 할인 정책

  • 기획자가 서비스 오픈 직전에 할인 정책을 정률제로 바꾸고 싶다고 하면 어떻게 해야할까?

 

  Ex) 

  - RateDiscountPolicy 클래스를 만든다.

<hide/>
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;
        }
        return 0;
    }
}

 

  - 테스트 클래스 만들기

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

 Note) 실행 결과 - 성공 케이스

 

 

  Ex) 실패 케이스

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

 

 Note) 실행 결과 - 실패 케이스

 

 

 

3.2 새로운 할인 정책과 문재점

 

  - 이제 OrderServiceImpl 클래스의 내용 중, FixDiscountPolicy => RateDiscountPolicy로 바꾸면 된다.

  - OCP, DIP 같은 객체지향 설계 원칙을 준수하지 못했다

    -> DIP: 추상 클래스 뿐만아니라 구체(구현) 클래스에도 의존하고 있다. 

    -> OCP: 현재 코드는 기능을 확장해서 변경하면 클라이언트 코드에 영향을 준다. => OCP 위반

기대했던 의존 관계

 

실제 의존관계

 

정책 변경

  - 정액제를 정률제로 변경하는 순간 OrderServiceImpl의 소스 코드도 함께 변경해야한다. => "OCP"위반

 

 

 

  - 그러면 인터페이스에만 의존하도록 다음과 같이 코드를 변경하면?

    -> NullPointerException이 발생한다. => 선언만 하고 생성을 하지 않았기 때문이다.

<hide/>
public class OrderServiceImpl implements  OrderService{

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

  Note) 해결 방안

    - 누군가가 클라이언트인 "OrderServiceImpl"에 "DiscountPolicy"의 구현 객체를 대신 생성하고 주입해줘야한다.

 

 

 

3.3 관심사의 분리

 

  - AppConfig클래스 만든다. 애플리케이션 전체에 대해 환경 설정하고 구성한다는 뜻이다.

    -> AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다. 

    -> AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입(연결) 해준다. => 'injection'

    -> MemberServiceImpl은 MemoryMemberRepository에서 쓴다.

 

  - MemberServiceImpl 클래스를 다음과 같이 수정한다.

    -> MemberRepository변수를 선언만하고 생성은 하지 않는다.

    -> 클래스의 생성자를 만든다. 생성자를 통해 MemberRepository에 뭐를 넣을지 정할 예정이다.

    -> 추상화에만 의존하도록 만든다.

 

<hide/>
package hello.core.member;
public class MemberServiceImpl implements  MemberService{
    private final 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);
    }
}

 

  - AppConfig

    -> MemberService를 호출하면 new MemberServiceImpl(new MemoryMemberRepository)가 생성된다.

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

 

  - OrderServiceImpl 클래스 내용을 수정한다. (impl은 기능을 실행하는 부분만 책임진다)

    -> final은 무조건 생성 또는 생성자를 통해서 할당되어야한다.

    -> OrderServiceImpl의 멤버변수 memberRepository와 discountPolicy 를 선언하기만 하면 오류 난다.

    -> 따라서, 선언한 다음에 OrderServiceImpl 의 생성자를 만들어주면 오류가 해결된다.

 

 

  - OrderApp 클래스 수정

<hide/>
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) {

        MemberService memberService = new MemberServiceImpl(null);
        OrderService orderService = new OrderServiceImpl(null, null);
        Long memberId = 1L;
        Member member = new Member(memberId, "itemA", 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());
    }
}

  Note) 실행 결과

 

 

  Ex) 위의 코드의 null 부분을 AppConfig를 쓰도록 바꾼다. 

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

        Long memberId = 1L;
        Member member = new Member(memberId, "itemA", 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());
    }
}

  Note) 실행 결과

    - OrderApp은 더이상 구체 클래스에 의존하지 않는다. 인터페이스에만 의존할 뿐이다.

 

  - MemberServiceTest 클래스도 수정

    -> AppConfig를 사용하도록 한다.

    -> @BeforeEach: 각 테스트 전에 무조건 실행되는 부분이다. 테스트 개수 만큼 돌아간다.

<hide/>
package hello.core.member;
import hello.core.AppConfig;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public 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 클래스도 수정한다.

<hide/>
package hello.core.order;
import hello.core.AppConfig;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class OrderServiceTest {
    MemberService memberService;
    OrderService orderService;
    @BeforeEach
    public void beforeEach(){
        AppConfig appConfig = new AppConfig();
        memberService = appConfig.memberService();
        orderService = appConfig.orderService();
    }
    @Test
    void createOrder(){
        // given
        Long memberId = null;
        Member member = new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(memberId, "itemA", 10000);
        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
    }
}

 

  Note) 실행결과

    - 이제 AppConfig를 통해 관심을 확실하게 분리했다.

    - AppConfig는 공연기획자와 같은 역할이다.

 

 

 

3.4 AppConfig 리팩토링 

 

  - 그런데 현재 AppConfig를 보면 중복이 있고 역할에 따른 구현이 잘 안 보인다는 문제가 있다.

기대하는 그림

 

  - AppConfig 클래스 - 구성 정보

    -> 메서드명에 따라 역할과 구현 클래스가 명확하게 보인다.

  =============코드 내용이 나중에 바뀔 예정 ===============

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

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

    public OrderService orderService(){ // 누군가 오더서비스를 조회하면?
        return new OrderServiceImpl( memoryMemberRepository(), discountPolicy());
    }

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

 

 

 

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

 

  Ex) 정액제 => 정률제

    - 할인 정책을 바꾸면 AppConfig의 discountPolicy 의 반환값만 RateDiscountPolicy로 바꾸면 된다.

    - OrderServiceImpl은 바꿀 필요가 없다.

 

  - OrderApp 클래스 수정

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

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

        Order order = orderService.createOrder(memberId, "itemA", 20000);
        System.out.println("order = " + order.toString());
    }
}

  Note) 실행결과

 

 

 

3.6 전체 흐름 정리 

  • 새로운 할인 정책 개발
    • 새로운 할인 정책 적용과 문제점 =>클라이언트가 DiscountPolicy 뿐만 아니라 'FixDisountPolicy'에도 의존 => 'DIP'위반
  • 관심사의 분리 => 'AppConfig'
  • AppConfig 리팩토링 - new를 하나만 나오도록한다(중복 제거). 역할과 구현을 분리
  • 새로운 구조와 할인 정책 적용

 

 

 

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

 

  SRP - 단일 책임 원칙(1 클래스 - 1 책임)

  • 구현 객체를 생성하고 연결하는 책임은 AppConfig가 담당한다.
  • 클라이언트 객체를 실행하는 역할만 담당

 

 

  DIP - 의존관계 역전 원칙(개발자는 구체화가 아닌 추상화에 의존하도록)

  • AppConfig가 'FixDiscountPolicy' 객체 인스턴스를 클라이언트 코드대신 생성해서 클라이언트 코드에 의존 관계를 주입해서 DIP를 지킬 수 있다.

 

 

  OCP - 개방 폐쇄 원칙

  • 애플리케이션을 사용 / 구성 영역으로 나눈다.
  • 소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀 있어야 한다.

 

 

 

3.8 IoC, DI,  컨테이너

 

  IoC(Inversion Of Control) - 제어의 역전

  • 기존의 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성, 연결, 실행했다. 즉, 구현 객체가 프로그램의 제어 흐름을 스스로 컨트롤했다. 
  • 반면에, AppConfig를 사용하면 구현 객체는 자신의 로직을 실행하는 역할만 담당한다.
  • ex) 프로그램을 제어하는 권한은 모두 AppConfig가 가지고 있다.  OrderServiceImpl 또한 AppConfig가 생성한다. 그리고 AppConfig는  OrderServiceImpl 가 아닌 OrderService 인터페이스의 다른 구현 객체를 생성하고 실행할 수 있다. 그런 사실과 무관하게 OrderServiceImpl는 자신의 로직을 실행한다.
  • 이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전이라고 한다.

 

 

  DI(Dependency Injection) - 의존 관계 주입

  • OrderServiceImpl은 DiscountPolicy 인터페이스에 의존한다. 실제 어떤 구현 객체가 사용될지 모른다.
  • 의존 관계는 정적인 클래스 의존 관계와 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계 둘을 분리해서 생각해야한다.
    • 정적인 클래스 의존 관계: import문만 봐도 분석 가능
    • 동적인 객체(인스턴스) 의존 관계: 애플리케이션을 실행하고나서 분석 가능하다.

 

 

프레임워크(Framework) vs  라이브러리(Library)

  • 프레임워크가 내 코드를 제어하고 대신 실행하면 프레임워크가 맞다. ex) JUnit
  • 내 코드가 제어의 흐름을 직접 담당하는 경우는 라이브러리

클래스 다이어그램

 

객체 다이어그램

 

객체 다이어그램

  • 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존 관계가 되는 것을 의존관계 주입이라고 한다.
  • 객체 인스턴스를 생성하고 그 참조값을 전달해서 연결된다.
  • 의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고 클라이언트가 호출하는 대상의 타입 인스턴스를 변경 가능하다. 

 

 

IoC 컨테이너,  DI 컨테이너

  • AppConfig 처럼 객체를 생성하고 관리하면서 의존 관계를 연결해주는 것을 'IoC 컨테이너' 또는 'DI 컨테이너' 라고 한다.
  • 최근에는 주로 'DI 컨테이너'라고 많이 한다.

 

 

3.9 스프링으로 전환하기 

 

  Ex) 지금까지 자바 프로젝트를 만든 것과는 다르게 스프링을 사용해보자

 

    - AppConfig 클래스에  @Configuration어노테이션을 붙인다. => 애플리케이션의 설정 정보를 담당한다.

      -> AppConfig 하위의 각 메서드에 @Bean을 붙인다.

      -> @Bean을 붙이면 그 메서드들이 모두 스프링 컨테이너에 모두 빈으로 등록된다. 기본적으로 메서드 이름(key)으로 등록된다. 메서드 반환값은 value이다.

      -> 또한, @Bean(name="메서드명 대신 붙이고 싶은 이름")을 이용해서 객체의 이름을 정할 수도 있다.

<hide/>
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;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService(){
        return new MemberServiceImpl(memoryMemberRepository());
    }

    @Bean
    public MemoryMemberRepository memoryMemberRepository() {
        return new MemoryMemberRepository();
    }

    @Bean
    public OrderService orderService(){ // 누군가 오더서비스를 조회하면?
        return new OrderServiceImpl( memoryMemberRepository(), discountPolicy());
    }

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

 

   - MmberApp

     -> ApplicationContext는 스프링 컨테이너라고 볼 수 있다. bean과 같은 객체들을 모두 관리한다.

     ->  new AnnotationConfigApplicationContext(AppConfig.class)

       => 클래스 AppConfig 안에 있는 환경 설정 정보를 가지고 스프링이 모든 빈을 스프링 컨테이너에 넣어서 관리해준다. 

      -> 이제, AppConfig가 아닌 ApplicationContext에서 빈을 찾아와야 한다.       

        => appliacationContext.getBean("memberService", MemberService.class);  ..("이름", "반환 타입")

        => 스프링 컨테이너 appliacationContext에서 이름이 "memberService"인 객체를 찾아서 MemberService 형태로 반환하겠다는 뜻이다.

<hide/>
package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
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);   // memberService를 찾겠다
        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());
    }
}

    

  Ex)  OrderApp으로 실습

<hide/>
package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.order.Order;
import hello.core.order.OrderService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
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, "itemA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(memberId, "itemA", 20000);
        System.out.println("order = " + order.toString());
    }
}

    Note) 실행 결과

 

 

 

스프링 컨테이너(Stpring Container)

  • ApplicationContext를 스프링 컨테이너라고 한다.
  • 기존에는 AppConfig를 사용해서 직접 객체를 생성하고 의존 관계 주입을 했지만 이제는 스프링 컨테이너를 사용한다.
  • @Configuration이 붙은 'AppConfig'를 설정(구성) 정보로 사용한다. Bean(@Bean이라고 적힌 객체)을 모두 호출해서 반환된 객체를 컨테이너에 등록한다.

 

 

출처 https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com