[스프링 핵심 원리 - 기본편] 객체 지향 원리 적용

dh·2023년 3월 20일
0
post-thumbnail


기존에 정액할인 정책을 사용하다가 정률 할인 정책으로 변경하는 상황

public class OrderServiceImpl implements OrderService {
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
 private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}

OrderServiceImpl에서 FixDiscountPolicy()RateDiscountPolicy()로 변경해줬다.

하지만 여기에 문제점이 있다

  1. DIP를 위반한다.
    주문서비스 클라이언트(OrderServiceImple)은 DiscountPolicy인터페이스(추상)뿐만 아니라 구체(구현)클래스에도 의존하고 있다.
    추상(인터페이스) : DiscountPolicy
    구체(구현) 클래스 : FixDiscountPolicy , RateDiscountPolic

  2. OCP를 위반한다.
    지금 코드는 기능을 확장해서 변경하면, 클라이언트(OrderServiceImple) 코드부분에서 RateDiscountPolicy로 변경하므로 클라이언트 코드에 영향을 준다.

결국 인터페이스에만 의존하도록 변경해야 한다.

public class OrderServiceImpl implements OrderService {
 //private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
 private DiscountPolicy discountPolicy;
}

문제점
위와 같이 인터페이스에만 의존 했지만 discountPolicy에 아무것도 없어서 null pointer exception이 뜨게 된다.

관심사의 분리

해결방안
이 문제를 해결하려면 누군가가 클라이언트인 OrderServiceImplDiscountPolicy의 구현 객체를 대신 생성하고 주입해주어야 한다.

위 코드를 공연에 비유하면 로미오 역할(인터페이스)을 하는 배우(구현체)가 줄리엣 역할을 하는 배우를 직접 캐스팅하는 것과 같다.
배우는 배역에만 집중하고, 공연과 캐스팅을 담당하는 기획자가 필요하다.

AppConfig

public class AppConfig {
	//MemberService 역할
   public MemberService memberService() {
   	return new MemberServiceImpl(memberRepository());
   }
   
    //OrderService 역할
   public OrderService orderService() {
     return new OrderServiceImpl(
           memberRepository(),
           discountPolicy());
   }
   
	//MemberRepository 역할
   public MemberRepository memberRepository() {
      return new MemoryMemberRepository();
   }
   
	// DiscountPolicy 역할
   public DiscountPolicy discountPolicy() {
      return new FixDiscountPolicy();
   }
}

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

  • MemberServiceImpl
  • MemoryMemberRepository
  • OrderServiceImpl
  • FixDiscountPolicy

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

  • MemberServiceImpl MemoryMemberRepository
  • OrderServiceImpl MemoryMemberRepository , FixDiscountPolicy
public class OrderServiceImpl implements OrderService {

   private final MemberRepository memberRepository;
   private final DiscountPolicy discountPolicy;
   
     /* AppConfig에서 생성자를 통해 구체클래스를 주입해서
        추상(인터페이스)에만 의존한다. -> DIP 지킴!
     */
   public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
     this.memberRepository = memberRepository;
     this.discountPolicy = discountPolicy;
   }
}

  • 객체의 생성과 연결은 AppConfig 가 담당한다.
  • DIP 완성: MemberServiceImplMemberRepository인 추상에만 의존하면 된다. 이제 구체 클래스를 몰라도 된다.
  • 관심사의 분리: 객체를 생성하고 연결하는 역할과 실행하는 역할이 명확히 분리되었다.

Appconfig에서 객체를 생성하고 그 참조값을 serviceImpl을 생성하면서 생성자로 전달한다.
클라이언트인 serviceImpl 입장에서 보면 의존관계를 외부에서 주입하는 것과 같다고 해서 DI(의존성 주입)이라 한다

AppConfig 등장으로 애플리케이션이 사용 영역과, 객체를 생성하고 구성하는 영역으로 분리 됨

DIP 해결
AppConfig가 구현객체를 생성해서 클라이언트의 생성자를 통해 의존관계를 주입해 주면서 DIP문제가 해결됨

OCP 해결
이제 OrderServiceImpl에서 FixDiscountPolicy RateDiscountPolic로 변경하던 것을 AppConfig에서 변경하면 되므로 사용영역의 코드를 변경할 필요가 없어 졌다.

제어의 역전 IoC(Inversion of Control)

  • 기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고, 실행했다.(구현 객체가 프로그램의 제어 흐름을 스스로 조종했다)

  • 반면에 AppConfig가 등장한 이후에 구현 객체는 자신의 로직을 실행하는 역할만 담당한다. 프로그램의 제어 흐름은 이제 AppConfig가 가져간다. 예를 들어서 OrderServiceImpl 은 필요한 인터페이스들을 호출하지만 어떤 구현 객체들이 실행될지 모른다.

  • 이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라 한다

스프링으로 전환

AppConfig

@Configuration
public class AppConfig {

    //MemberService 역할
    @Bean
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }
    //MemberRepository 역할
    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
    //OrderService 역할
    @Bean
    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
    // DiscountPolicy 역할
    @Bean
    public DiscountPolicy discountPolicy(){
        // AppConfig에서 RateDiscountPolicy로만 교체하면 됨
        return new RateDiscountPolicy();
    }
}
public class MemberApp {
    public static void main(String[] args) {
    	// 순수한 자바로 AppConfig 사용했을떄
        // AppConfig appConfig = new AppConfig();
		//MemberService memberService = appConfig.memberService();
		//OrderService orderService = appConfig.orderService();
		
        //스프링으로 AppConfig 사용했을때
        // 스프링 컨테이너에 Bean들을 넣고 관리해줌
        ApplicationContext ac= new AnnotationConfigApplicationContext(AppConfig.class);
        //getBean(@Bean의 이름, 반환 타입)으로 필요한 스프링 빈 찾기
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        OrderService orderService = ac.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.toString());
    }
}

스프링 컨테이너

  • ApplicationContext를 스프링컨테이너라고 한다.
  • 기존에는 개발자가 AppConfig로 직접 DI를 했지만 이제부터는 스프링컨테이너를 통해 한다.
  • 스프링 컨테이너는 @Configuration 이 붙은 AppConfig를 설정 정보로 사용한다. 여기서 @Bean이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다

출처 김영한 스프링 핵심 원리 - 기본편

0개의 댓글