[Spring] 기본편 03. 스프링 핵심 원리 이해2 - 객체 지향 원리 적용

47401 단어 SpringSpring

이 글은 스프링 핵심 원리 - 기본편 을 듣고 정리한 내용입니다.

📌 새로운 할인 정책 개발

  • 정률 할인 정책으로 바꾸고자 함. (구매 금액에 따라 할인율이 달라지는 정책)

  • RateDiscountPolicy.class 추가
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;
        }
    }
}
  • RateDiscountPolidcyTest.class 추가
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(1000);
    }

}
  • RateDiscountPolicy 정책으로 변경
    - OrderServiceImple 파일에서 기존 정책을 주석처리하고, 새로운 정책을 생성.
//    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
      private final DiscountPolicy discountPolicy = new RateDiscountPolicy();

❗️ 문제점 발견

  • 클래스 의존관계를 보면, 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있다.
  • OrderServiceImpl에서
    -> 추상(인터페이스) 의존 : DiscountPolicy
    -> 구체(구현)클래스 의존 : FixDiscountPolicy, RateDiscountPolicy
  • 즉,지금 코드는 기능을 확장해서 변경하면, 클라이언트 코드에 영향을 준다. 따라서 OCP 위반.
  • 그리고, 구체 클레스를 변경할 때, 클라이언트 코드도 변경해야 하므로 DIP 위반

❓ 어떻게 해결?

  • 인터페이스에만 의존하도록 설계를 변경하자.
  • 인터페이스에만 의존하도록 코드 변경
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;
}
  • 인터페이스에만 의존을 하긴 하지만 구현체가 없어서 코드를 실행하면 NPE(Null Pointer Exception) 발생.
  • 해결방안 : 누군가 클라이언트인 OrderServiceImpl에 DiscountPolicy의 구현객체를 대신 생성하고 주입해주어야 함.

📌 관심사의 분리

  • 관심사를 분리해야 한다.
  • 공연을 예로 들면, 배우는 본인 역할인 배역을 수행하는 것에만 집중 해야 한다.
  • 공연을 구성하고, 담당 배우를 섭외하고, 역할에 맞는 배우를 지정하는 책임을 담당하는 별도의 공연 기획자가 있어야 한다.
  • 공연 기획자를 만들고, 배우와 공연 기획자의 책임을 확실히 분리하자 -> 그 역할을 AppConfig가 한다.

🌱 AppConfig 등장

  • 애플리케이션 전체 동장 방식 구성(Config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스 이다.

  • MemberServiceImpl - 생성자 주입
    -> MemberServiceImpl 내에서 MemberRepository 구현체를 new로 생성하는것이 아니라 생성자를 통해 MemberRepository를 주입받도록 변경하였다.

  • 이렇게 설계 변경으로 인해 MemberServiceImpl은 MemoryMemberRepository 를 의존하지 않는다.

  • MemberServiceImpl의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정된다.

public class MemberServiceImpl implements  MemberService{

    private final MemberRepository memberRepository;
    //memberRepository interface필요, but interface만 가지고 있으면 nullPointerException 터질것임.

    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
  • AppConfig.class
public class AppConfig {
    
    public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository());
    }
}
  • OrderServiceImple도 같은 방식으로 수정한다. (구현체 받는 부분 제거 및 생성자 생성)
public class OrderServiceImpl implements OrderService {

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

    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
  }
  • AppConfig.class에 OrderService 부분 추가
    public OrderService orderService(){
        return new OrderServiceImpl( new MemoryMemberRepository(), new FixDiscountPolicy());
    }

🌱 설계변경으로 인해 이전과 달라진점

  • OrderServiceImpl의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정한다.

  • OrderServiceImpl은 이제부터 '의존관계에 대한 고민은 외부'에게 맡기고, '실행에만 집중'하면 됨.


  • 객체의 생성과 연결은 AppConfig가 담당

  • DIP 완성 : MemberServiceImpl은 MemberRepository인 추상에만 의존하면 된다. (구체 클래스는 이제 몰라도 됨)

  • 관심사의 분리 성공
    -> 객체를 생성하고 연결하는 역할(AppConfig)실행하는 역할(Service)이 명확히 분리되었다.

🌱 의존관계 주입

  • appConfig 객체는 memoryMemberRepository 객체를 생성하고 그 참조값을 memberServiceImpl을 생성하면서 생성자로 전달한다.
  • 클라이언트인 memberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고 해서 DI(Dependency Injection) , 즉 의존관계 주입 또는 의존성 주입이라 한다.

  • MemberApp과 OrderApp이 더이상 구체클레스에 의존하지 않도록 코드 수정
    - AppConfig를 생성하여, AppConfig에 의존하여 필요한 service를 생성한다.
public class MemberApp {
    public static void main(String[] args) {
        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();
        }
      }

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

        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();
        OrderService orderService= appConfig.orderService();
        }
      }
      

🌱 Test Code 수정

  • @BeforeEach는 각 테스트 실행 전에 무조건 실행되는 부분
    ex) 테스트가 3개 있으면 총 3번 실행됨.
  • TestCode 들도 마찬가지로 Appconfig에 의존하여 service를 받는 방식으로 수정한다.
public class MemberServiceTest {

    MemberService memberService;

    @BeforeEach // 각 테스트 실행 전에 무조건 실행되는 부분
    public void beforeEach() {
        AppConfig appConfig = new AppConfig();
        memberService = appConfig.memberService();
    }

}


public class OrderServiceTest {

    MemberService memberService;
    OrderService orderService;

    @BeforeEach // 각 테스트 실행 전에 무조건 실행되는 부분
    public void beforeEach() {
        AppConfig appConfig = new AppConfig();
        memberService = appConfig.memberService();
        orderService = appConfig.orderService();
    }

}

📌 AppConfig 리팩토링

  • 현재 AppConfig에는 new의 중복이 있고, 역할에 따른 구현이 잘 안보임.
  • 중복을 제거하고, 역할에 따른 구현이 잘보이게 해야한다.

  • AppConfig.class 수정
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();
    }


}
  • new MemoryMemberRepository() 중복부분이 제거됨. 이제 MemoryMemberRepository를 다른 구현체로 변경할 때 한부분만 변경하면 된다.
  • 이제 AppConfig를 보면 각각의 객체가 무엇을 리턴하는지 한눈에 파악할수 있다. (변경시, 리턴 부분만 변경해주면 됨)
  • 역할과 구현 클래스가 한눈에 들어온다. -> 애플리케이션 전체 구성을 빠르게 파악할 수 있다.

📌 할인 정책 변경

  • 기존에 FixDiscountPolicy를 RateDiscountPolicy로 변경해 보겠다.
  • AppConfig.class파일에서 다음 부분만 변경하면 끝이다 .
 public DiscountPolicy discountPolicy(){
        //return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
  • 할인 정책을 변경할 때, 애플리케이션의 구성 역할을 담당하는 AppConfig만 변경하면 된다. (구성영역)
  • 클라이언트 코드인 OrderServiceImpl를 포함해서 사용 영역의 어떤 코드도 변경할 필요가 없다.
  • 구성 역할을 담당하는 AppConfig는 공연에서 기획자의 역할과 같다. 기획자는 공연 참여자인 구현 객체들을 모두 알아한다.

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

  • 지금까지 개발내용에서는 이중에 3가지인 SRP, DIP, OCP이 적용된 것이다

  • 1. SRP - 단일 책임 원칙

    • 단일 책임 원칙을 따르면서 관심사를 분리하였다.
    • 구현 객체를 생성하고, 연결하는 책임은 AppConfig가 담당하고, 클라이언트 객체는 실행하는 책임만 담당한다.
  • 2. DIP - 의존관계 역전 원칙
    - 프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다"는 원칙을 다르는 방법이다.

    • 이전 개발에서는 클라이언트에서 추상화 인터페이스인 DiscountPolicy에도 의존하였지만 구체화 구현 클래스인 FixDiscountPolicy에도 의존하였다.
    • 그래서 클라이언트 코드가 인터페이스에만 의존하도록 변경하였으나 NPE이 발생하여 아무것도 실행할 수 없었다.
    • 그래서 AppConfig에서 구체화 객체 인스턴스를 클라이언트 코드 대신 생성해서 클라이언트 코드에 의존관계를 주입하였다.
    • 이렇게 DIP 원칙을 따를 수 있게 되었다.
  • 3.OCP - 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
    - 애플리케이션을 사용 영역과 구성 영역으로 나누었다.

    • AppConfig가 의존관계를 FixDiscountPolicy -> RateDiscountPolicy로 변경해서 클라이언트 코드에 주입하므로 클라이언트 코드를 변경하지 않아도 된다.
    • 즉 소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀있게 된 것이다!

📌 IOC, DI, 그리고 컨테이너

🌱 제어의 역전 IoC(Inversion of Contorl)

  • 기존 프로그램에서는 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고 실행하였다.
  • 그러나, AppConfig를 통해 구성 영역과 사용 영역이 구분되었고, 프로그램 제어 흐름에 대한 권한
    은 AppConfig가 가지고 있다.
    • 이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는것을 제어의 역전(IoC)이라 한다.

🌱 프레임워크 vs 라이브러리

  • 프레임워크 : 내가 작성한 코드를 프레임워크가 제어하고, 대신 실행한다
  • 라이브러리 : 내가 작성한 코드가 직접 제어의 흐름을 담당하면 그것은 내가 사용한 라이브러리 이다.
    -> 즉 제어의 주도권이 나에게 있으면 라이브러리, 그것에게 있으면 프레임워크.

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

  • 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는것을 의존관계 주입 이라한다.
  • 의존관계 주입을 사용하면 정적인 클래스 의존관계(ex. OrderService - MemberRepository - DiscountPolidcy)를 변경하지 않고, 동적인 객체 인스턴스 의존관계 (ex. 어떤 회원저장소, 어떤 할인정책)를 쉽게 변경 할 수 있다.

🌱 IoC 컨테이너, DI 컨테이너

  • AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 IoC 컨테이너(=DI 컨테이너)라 함.
  • 최근에는 주로 DI 컨테이너 라고 불림.

📌 스프링으로 전환하기

  • 지금까지 자바로만 작성하였는데 스프링을 사용해보도록 하자.

  • AppConfig.class를 스프링 기반으로 변경

    • 기존에서 달라진 점은, AppConfig클래스에 @Configuration 어노테이션이 추가 되었고, 각각의 메서드마다 @Bean 어노테이션이 추가 되었다.
    • 이렇게 함으로써, 스프링 컨테이너에 스프링 빈으로 등록한다.
@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에 스프링 컨테이너 적용
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("findMember = "+findMember.getName());
    }
}
  • OrderApp에 스프링 컨테이너 적용
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);


      //  MemberService memberService = new MemberServiceImpl();
      //  OrderService orderService= new OrderServiceImpl();

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

        Order order = orderService.createOrder(memberId, "itemA", 20000);

        System.out.println("order = "+ order);
        System.out.println("order.calculatePrice= "+order.calculatePrice());
    }
}

🌱 스프링 컨테이너

  • ApplicationContext스프링 컨테이너 라고 한다.
  • 스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정(구성)정보로 사용한다.
    • 여기서 @Bean 어노테이션이 붙은 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다.
    • 스프링 컨테이너에 등록된 객체들을 스프링 빈이라고 한다.
  • 스프링 빈은 @Bean 이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다.
  • 스프링 빈은 applicationContext.getBean()메서드로 찾을 수 있다.

❓코드가 더 복잡해진것 같은데 ,스프링 컨테이너를 왜 사용할까?

  • 기존에는 개발자가 직접 자바코드로 모든것을 했다면 이제부터는 스프링 컨테이너에 객체를 스프링 빈으로 등록하고, 스프링 컨테이너에서 빈을 찾아서 사용하도록 변경되었다.
  • 스프링 컨테이너를 사용하면 뭐가 좋은지는 다음 강에서 이어서 배워볼 예정~

좋은 웹페이지 즐겨찾기