5. 객체 지향 원리 적용

Yuri JI·2023년 1월 9일
0

❗ 주문 서비스의 클라이언트(OrderServcieImpl)가 구현 객체인 FixDiscountPolicy에 의존하고 있다. = OrderServiceImpl에서 FixDiscountPolicy 객체를 생성하고 있다. (new FixDiscountPolicy)

  • 이번 시간
    • 새로운 할인 정책이 추가되었다 → DIP, OCP 위배
    • 지난 시간 코드에 객체지향개념을 잘 적용하여 문제를 해결해보자
    • 스프링의 핵심 기능인 스프링 컨테이너가 왜 생겼는지 이해해보자

새로운 할인 정책 개발

기획자 : 기존의 고정 금액 할인이 아닌 주문 금액 당 할인하는 정률 할인으로 변경해주세요.

개발자 : RateDiscountPolicy(정률 할인)만 추가로 개발해서 쓱 바꿔주면 되겠다 !

  • ReateDiscountPolicy
    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;
            }
        }
    }
  • (테스트) RateDiscountPolicyTest
class RateDiscountPolicyTest {

    /*
     이 Policy가 정말 10퍼센트 할인이 되는지 테스트
     성공 테스트 작성 + 실패 테스트도 만들어 봐야함
     */

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

새로운 할인 정책 적용과 문제점

  • 새로 작성한 RateDiscountPolicy를 적용해보자
    public class OrderServiceImpl implements OrderService {
    	**// private final DicountPolicy discountPolicy = new FixDiscountPolicy();**
    		 **private final DicountPolicy discountPolicy = new RateDiscountPolicy();**
    }
  • 문제점 발견 → 할인 정책을 변경하려면 클라이언트인 OrderServiceImpl을 고쳐야한다.
    • 우리는 역할(인터페이스)과 구현(구현 객체)을 충실하게 분리하고, 다형성을 활용했다.
    • 하지만, OCP, DIP 같은 객체지향 설계 원칙을 충실히 준수하지 못했다.
      • 준수한 것처럼 보이지만 아님 !
      • DIP : 주문서비스의 클라이언트 OverServiceImplDiscountPolicy 인터페이스에 의존함.
        • 클래스 의존관계를 분석해보면, 인터페이스 뿐만 아니라 구체 클래스에도 의존하고있다. FixDiscountPolicy & RateDiscountPolicy → 인터페이스(추상)와 구체 클래스(구체)에 모두 의존해서 DIP 위반
      • OCP : 변경하지 않고 확장할 수 있다.
        • 지금 코드는 할인 정책을 변경하는 순간(FixDiscountPolicyRateDiscountPolicy로 변경), 클라이언트 코드(OrderServiceImpl)에 영향을 준다.
    • DIP 위반했기때문에 할인 정책을 변경하는 순간 OCP도 위반하게 된다.
      • OCP 위반 : 기능을 확장/변경하면, 클라이언트 코드에 영향을 준다.

❗ 이 문제를 어떻게 해결할 것인가

  1. 클라이언트 코드인 OrderServiceImplDiscountPolicy의 인터페이스 뿐만 아니라 구체 클래스도 함께 의존하고 있음
    public class OrderServiceImpl implements OrderService {
    	// 인터페이스 = new 구체 클래스
    	private final DicountPolicy discountPolicy = new RateDiscountPolicy();
    }
  1. 그래서 구체 클래스를 변경할 때 클라이언트 코드도 함께 변경해야 한다.
  2. “DIP”위반 → 추상에만 의존하도록 변경
  3. 즉, DIP를 위반하지 않도록 인터페이스에만 의존하도록 의존 관계를 변경하자
    public class OrderServiceImpl implements OrderService {
    		 private DicountPolicy discountPolicy;
    }
  1. 인터페이스에만 의존하지만 구현체가 없다 !

    → 실행하면 NPE(Null Pointer Exception) 발생

⭐ 6. 결국, 이 문제를 해결하려면 누군가가 클라이언트(OrderServiceImpl)에 DiscountPolicy구현 객체를 대신 생성하고 주입해주어야 한다.

구현 객체를 대신 생성하고 주입하는 방법 > 관심사의 분리

공연을 예로 들면, 배역을 인터페이스라고할 때

배우(구현 객체)가 공연 뿐만 아니라 다른 역할(인터페이스)의 배우(구현체)까지 캐스팅하는 것은 하나의 배우(구현 객체)가 “다양한 책임”을 가지고 있는 것이다.

관심사를 분리하자 !

배우는 본인의 역할인 배역을 수행하는 것에만 집중해야한다.

배역의 배우를 지정하는 일(책임)은 별도의 공연 기획자가 할 일이다.

따라서, 공연 기획자를 만들고, 배우와 공연 기획자의 책임을 분리해보자

AppConfig (공연 기획자, 객체 생성과 연결을 담당)

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

왜 AppConfig가 필요한지

public class MemberServiceImpl implements MemberService {
	private final MemberRepository memeberRepostiroy = new MemoryMemberRepository(); (1)
}

(1) MemberServiceImpl 에는 MemberRepostiroy(인터페이스, 배역)과 동시에 MemoryMemberRepository(구현체)가 존재한다.

이것은 MemberServiceImplMemoryMemberRepository 를 선택한 것이고 배우가 배역의 배우를 직접 지정한 것과 같다.

⭐ 배우가 다양한 책임을 지지않도록, AppConfig를 통해 배우(구현 객체)를 지정하자 !

AppConfig를 통한 생성자 주입

@Configuration
public class AppConfig {

    public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository()); (1)
    }
}

(1) MemberServiceImpl이 직접 구현체를 지정하지 않고 AppConfig에서 지정할 수 있도록 함수를 생성하고, MemberServiceImpl에서 사용할 구현체를 매개 변수에 명시해준다.

public class MemberServiceImpl implements MemberService {

	private final MemberRepository memeberRepostiroy; (1)
	
	public MemberServiceImpl(MemberRepository memberRepository) { (2)
		this.memberRepository = memberRepotiroy;
	}
}

(1) 구현체를 지우고 인터페이스만 남도록한다.

(2) MemberRepository의 구현체를 생성자 함수의 매개변수로 받아온다.

❗ 이렇게하면 MemberServiceImpl 에는 MemoryMemberRepository가 존재하지 않는다. = 인터페이스(추상화)에만 의존한다 ! = DIP 준수

MemberServiceImpl 의 구현체를 밖에서 생성한 뒤, 생성자를 통해 값을 할당 받을 수 있기 때문에 생성자 주입이라고 한다.

OrderServiceImpl도 변경해보자

public class OrderServiceImpl implements OrderService{ 

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

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

(1) OrderServiceImpl 구체적인 클래스를 전혀 알 수 없다. FixDiscountPolicy()가 들어올지 RateDiscountPolicy()가 들어올지 전혀 알 수 없다.

@Configuration
public class AppConfig { 

    public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository()); (1)
    }

		public OrderService orderService() {
				return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
		}
}(1)

(1) ⭐⭐⭐ AppConfig는 애플리케이션 실제 동작에 필요한 “구현 객체를 생성”한다. (new 키워드) 이렇게 생성된 객체 인스턴스의 참조(레퍼런스)를 “생성자를 통해서 주입(연결) “해준다. (파라미터를 통해)
=> appConfig객체는 memoryMemberRepository 객체를 생성하고 그 참조값memberServiceImpl을 생성하면서 생성자로 전달한다.
=> 이를 DI(의존관계 주입)이라고한다.

MemberServiceImpl MemoryMemberREpository

OrderServiceImpl MemoryMemberRepository, FixDiscountPolicy

중간결론

  • 설계를 변경함으로써 MemberServiceImplMemoryMemberREpository 를 의존하지 않는다 ! → MemberRepository 인터페이스에만 의존한다. ⇒ DIP 준수

  • MemberServiceImpl 의 입장에서는 생성자를 통해 어떤 구현 객체가 들어올지(주입될지) 알 수 없다.

  • MemberServiceImpl 의 생성자를 통해서 어떤 구현 객체를 주입할지는 외부(AppConfig)에서 결정된다.
    -> AppConfig가 객체의 생성과 연결을 담당한다 !

  • MemberServiceImpl 은 이제부터 의존 관계에 대한 고민은 외부에 맡기고(= 추상에만 의존하고, 구체 클래스를 몰라도 된다) 실행에만 집중할 수 있다.

  • 설계 변경으로 OrderServiceImplFixDiscountPolicy를 의존하지 않음!
  • 단지, DiscountPolicy 인터페이스만 의존한다.
  • 최종적으로는 OrderServiceImpl에는 MemoryMemberRepository, FixDiscountPolicy 객체의 의존 관계가 주입된다.

AppConfig 실행

AppConfig를 실행해보며 의존 관계에 대해 생각해보자.

public class MemberApp {
	public static void main(String[] args){
		AppConfig appConfig = new AppConfig();
//  MemberServie memeberService = new MemberServiceImpl(); (1)
		MemberServie memberService = appConfig.memberService(); (2)
		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());
	}
}

(1) 기존엔 직접 MemberServiceImpl을 생성하고, 그 다음에 MemberServiceImpl에서 FixDiscountPolicy를 순차적으로 생성함

(2) 생성자 주입 후에는 AppConfig 에게 MemberService 를 달라고 하면 인터페이스의 구현체 들을 넘겨준다.

AppConfig 테스트

public class MemberServiceTest {

    MemberService memberService;

    @BeforeEach // 테스트 실행 전에 무조건 실행되는 것
    public void beforeEach() {
				// 가장 먼저 AppConfig를 생성해주자 
        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);
    }
}

AppConfig가 구체 클래스를 선택해주니까 인터페이스들은 구현체를 몰라도 괜찮고 실행하는 책임만 지면 된다.

AppConfig 리팩터링

현재 AppConfig는 “중복"이 있고, “역할"에 따른 “구현”이 잘 안 보인다. ⇒ 구성 정보에는 역할에 따른 구현이 잘 보이는 것이 중요하다.

// 현재 AppConfig
@Configuration
public class AppConfig { 

    public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository()); (1)
    }

	public OrderService orderService() {
		return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
	}
}
  • 리팩터링 방법
    • Extract Method : cmd + option + M
    • new MemoryMemberRepository 부분을 드래그 한 뒤, Extract Method으로 memberRepository 함수 생성
    • 이렇게하면 Method명과 리턴 타입만 보아도 역할을 알 수 있다.
    • 그리고 비즈니스 요구사항의 변경이 있어도(구현체를 변경해야할 때) 해당 부분만 수정하면 된다.
    • 또한 역할과 구현 클래스가 한눈에 들어오기 때문에, 애플리케이션 전체 구성이 어떻게 되어있는지 빠르게 파악할 수 있게 되었다.
public class AppConfig {
    public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository());
    }

    // return type은 인터페이스(역할)가 되도록 작성
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

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

새로운 구조와 할인 적용

이제 할인 정책을 고정 할인 정책에서 정률 할인 정책으로 변경하자

어떤 부분을 변경해야할지 고민해보자

기존에는 클라이언트 코드까지 변경했어야하지만, AppConfig의 등장으로 애플리케이션이 크게 사용 영역 / 구성 영역(객채 생성 및 구성)으로 분리되었다.

AppConfig에 새로운 할인 정책 적용

public DiscountPolicy discountPolicy() {
//        return new FixDiscountPolicy(); 
        return new RateDiscountPolicy(); (1)
}

(1) 기존 할인 정책 대신 새로운 할인 정책의 구현체를 생성 후 리턴한다.

❗ 이제 할인 정책을 변경해도, 클라이언트 코드(OrderServiceImpl)를 포함한 사용 영역의 어떠한 코드도 변경할 필요가 없다 ! 구성 영역의 AppConfig만 변경하면 된다.

profile
안녕하세요 😄

0개의 댓글