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

강은서·2022년 7월 18일
0

Spring

목록 보기
10/11
post-thumbnail

지난 시간에 예제를 작성하면서, 궁금증이 생긴 부분은 어떻게 객체 지향 원리인 DIP/OCP 그리고 다형성을 지키면서 개발을 할 수 있을까였다.궁금증이 극대화 된 상태라 이번 강의를 아주 흥미롭게 들을 수 있었다.


인터페이스 DiscountPolicy를 구현한 FixDiscountPolicy를 RateDiscountPolicy로 변경해야하는 상황이 주어졌다.

새로운 할인 정책 적용

할인정책 변경

  • RateDiscountPolicy 비율 할인 구현체 구현
private int discountPolicy = 10;

@Override 
public int discount(Member member, int price){
	if(member.getGrade() == Grade.VIP){
    	return price * discountPolicy / 100;
    }else{
    	return 0;
    }
}
  • 변경된 할인 정책 클라이언트 코드에 적용
public class OrderServiceImpl implements OrderService{
	//private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
	private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}

할인 정책 변경에 관한 문제 발생

  1. DIP위반 : 주문 서비스 클라이언트 (OrderServiceImpl)는 추상 인터페이스(DiscountPolicy)뿐만 아니라 구현 클래스(FixDiscountPolicy, RateDiscountPolicy)에도 의존하고 있다.
  2. OCP위반 : 또한, 변경하기 위해서는 OrderServiceImpl를 변경해야 한다.

할인 정책 변경에 관한 문제 해결 방법

현재 발생하는 문제의 원인은 클라이언트가 인터페이스 뿐만 아니라 구현 클래스도 함께 의존한다는 점이다. 그래서, 해결방법은 인터페이스에만 의존하도록 의존 관계를 변경하면 된다.

  • 인터페이스에만 의존하도록 코드 변경
public class OrderServiceImpl implements OrderService{
	//private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
    private DiscountPolicy discountPolicy;
}

하지만, 다음 코드는 구현체가 없기 때문에 NPE가 발생한다. 이 문제를 해결하기 위해서는 누군가가 클라이언트인 OrderServiceImpl에 DiscountPolicy의 구현 객체를 대신 생성하고 주입해주어야 한다.

AppConfig의 등장

그 누군가에 해당하는 것이 AppConfig이다.
애플리케이션의 전체 동작 방식을 구성하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스이다.

  • AppConfig
public class AppConfig{
	public MemberService memberService(){
    	return new MemberServiceImpl(new MemoryMemberRepository());
    }
    
    public OrderService orderService(){
    	return new OrderServiceImpl(new MemoryMemberRepository(),
        	new RateDiscountPolicy());
    }
- AppConfig는 애플리케이션의 실제 동작에 필요한 객체를 생성한다.
	- MemberServiceImpl
    - MemoryMemberRepository
    - OrderServiceImpl
    - RateDiscountPolicy
- AppConfig는 생성한 객체 인스턴스의 참조를 생성자를 통해서 주입한다.
	- MemberServiceImpl -> MemoryMemberRepository
    - OrderServiceImpl -> MemoryMemberRepository , RateDiscountPolicy
    

DI(Dependency Injection) 의존관계 주입

클라이언트 입장에서 의존 관계를 마치 외부에서 주입해주는 것처럼 보이는 것

  • MemberServiceImpl 생성자 주입
	private final MemberRepository memberRepository;
    public MemberServiceImpl(MemberRepository memberRepository){
    	this.memberRepository = memberRepository;
    }


AppConfig는 MemoryMemberRepository 객체를 생성하고, 그 참조값을 MemberServiceImpl을 생성하면서 생성자로 전달한다.

  • OrderserviceImpl 생성자 주입
	private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy){
    	this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

AppConfig 실행

  • MemberApp

public static void main(String[] args){
	AppConfig appConfig = new AppConfig();
    //AppConfig에서 memberService 객체를 만들고, memberRepository를 생성한다. 
    MemberService memberService = appConfig.memberService();
    Member member = new Member(1L, "memberA", Grade.VIP);
    memberService.join(member);
}
  • OrderApp
public static void main(String[] args){
	AppConfig appConfig = new AppConfig();
    MemberSerivce memberService = appConfig.memberService();
    OrderService orderService = appConfig.orderService();
   
    Member member = new Member(1L, "memberA", Grade.VIP);
    memberService.join(member);
    
    Order order = orderService.createOrder(1L, "itemA", 10000);
    
}

테스트 코드를 실행할 때 마다 @beforeEach를 사용하여, 객체를 생성해줄 수 있다.

AppConfig 리팩터링

현재, AppConfig에는 MemoryMemberRepository가 두번 생성되는 중복이 있고, 역할에 따른 구현이 잘 보이지 않는다. 다음과 같이 코드를 변경할 수 있다.

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

예제로 보는 좋은 객체 지향 설계의 5가지 원칙의 적용

SRP 단일 원칙 책임

한 클래스는 하나의 책임만 가져야 한다.

  • AppConfig : 구현 객체를 생성하고 연결하는 역할
  • 클라이언트 코드 : 객체를 실행하는 역할

DIP 의존 관계 역전 책임

프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다.

  • 클라이언트가 'RateDiscountPolicy'나 'FixDiscountPolicy'에 의존하지 않고, 'DiscountPolicy'에만 의존하도록 코드를 변경하였다.
  • AppConfig가 'FixDiscountPolicy'객체 인스턴스를 클라이언트 코드 대신 생성하여 클라이언트 코드에 주입하였다.

OCP 개방-폐쇄 원칙

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

  • 애플리케이션을 사용영역과 구성 영역으로 나눠 관심사를 분리하였다.
  • 다형성을 사용하여 클라이언트가 DIP를 지킨다.
    -> AppConfig가 의존관계를 변경하여 클라이언트 코드에 주입하므로 클라이언트 코드는 변경하지 않아도 된다.

IoC, DI, 그리고 컨테이너

제어의 역전 IoC(Inversion of Control)

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

기존에 클라이언트 코드에서 객체를 생성하고, 객체를 연결하고, 실행했지만 AppConfig를 통해서 객체를 생성하고, 클라이언트 코드에 주입해준 덕분에 클라이언트 코드는 어떤 구현 객체들이 실행될지 모르는 상태에서 단지 필요한 인터페이스를 호출하는 등 자신의 로직을 실행한다.

프레임워크 vs 라이브러리

  • 프레임 워크 : 내가 작성한 코드를 제어하고, 대신 실행한다.
  • 라이브러리 : 내가 작성한 코드가 직접 제어한 흐름을 담당한다.

의존 관계 주입 DI

  • 정적인 클래스 의존 관계
    클래스가 사용하는 import 코드만으로 정적인 의존 관계를 쉽게 판단할 수 있다.
    OrderServiceImpl는 DiscountPolicy와 MemberRepository를 의존한다는 것을 알 수 있다.

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

IoC컨테이너, DI컨테이너

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

다음 글은 인프런 김영한 강사님의 스프링 강의 복습용입니다 :)

0개의 댓글