김영한 스프링 핵심 원리(기본편) - 객체지향 원리적용

개발할래·3일 전
0

개발

목록 보기
13/14
post-thumbnail

객체 지향의 원리 적용 해보기

1. 새로운 할인 정책

금액의 %만큼 할인하는 정률 할인 정책 적용

  • RateDiscountPolicy 구현체 생성
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;
    }
}
  • RateDiscountPolicyTest 생성
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;

public 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);

    }

    @Test
    @DisplayName("VIP가 아니면, 할인이 적용되지 않아야 한다.")
    void vip_x(){
        // GIVEN
        Member member = new Member(1L, "memberBasic", Grade.BASIC);

        //when
        int discount = discountPolicy.discount(member, 10000);

        //then
        Assertions.assertThat(discount).isEqualTo(0);


    }
}

2. 정률 할인 정책 적용 및 문제점

  • OrderServiceImpl에서 할인 정책에 대한 구현체 변경 필요
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    //private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
 
 ... 생략
 
}    

👀 문제점?

☑️ 첫째, DIP 위반 : 클래스 간 의존관계 확인 -> 인터페이스(추상)와 구현 클래스 둘다 의존
☑️ 둘째, OCP 위반 : 코드를 변경하지 않고 확장? 기능을 추가 하여 변경 시 클라이언트 코드에 영향을 줌

☑️ 의존 관계 분석(클래스 다이어그램)

  • 기대했던 의존 관계

    • DiscountPolicy 인터페이스에만 의존한다고 생각함.
  • 실제 의존 관계

    • DiscountPolicy와 FixDiscountPolicy 둘다 의존 (DIP 위반)
    • DIP 위반 하게되면 OCP 위반도 같이 발생

🗝️ 문제 해경 방안

  • DIP 위반 -> 인테페이스에만 의존 하도록 변경
public class OrderServiceImpl implements OrderService {

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

근데 구현체가 없는데 정상적으로 코드가 실행 될까? NO!!
클라이언트인 OrderServiceImpl에 DiscountPolicy의 구현 객체를 생성하고 주입해야 한다.
그렇다면 어떻게?

3. 관심사의 분리

  • AppConfig
    애플리케이션의 전체 동작 방식을 설정ㆍ구성(config)하기 위해, 구현 객체를 생성하고 연결하는 책임을 가지는 별도의 설정 클래스 생성
public class AppConfig {
    
    public MemberService memberService(){
        return new MemberServiceImpl(new MemoryMemberRepository());
    }

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

- AppConfig는 애플리케이션의 실제 동작에 필요한 구현객체를 생성
(MemberServiceImpl, MemoryMemberRepository, OrderServiceImpl, FixDiscountPolicy)
- AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입
MemberServiceImpl : MemoryMemberRepository
OrderServiceImpl : MemoryMemberRepository, FixDiscountPolicy

  • MemberServiceImpl
public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    ... 생략

- MemberRepository 인터페이스에만 의존(DIP)
- MemberServiceImpl는 생성자를 통해서 어떤 구현 객체가 주입 될지 알 수 없음 -> 외부(AppConfig)에 의해 결정

  • 클래스 다이어그램

    • AppConfig : 객체의 생성과 연결 담당
    • DIP 달성 : MemberServiceImpl은 MemberRepository인 인터페이스에만 왼존
    • 관심사의 분리 : 객체를 생성 및 연결하는 역할 -> 역할이 명확하게 분리
  • 회원 객체 인스턴스 다이어그램

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

    public class OrderServiceImpl implements OrderService {
    
      private final MemberRepository memberRepository;
      //private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
      //private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
      private final DiscountPolicy discountPolicy;
    
      public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
          this.memberRepository = memberRepository;
          this.discountPolicy = discountPolicy;
      }
  • 설계 변경으로 DiscountPolicy 인터페이스만 의존한다.
  • OrderServiceImpl에는 MemoryMemberRepository, FixDiscountPolicy 객체의 의존관계 주입
  • AppConfig 적용 및 실행
    - MemberApp
public class MemberApp {
    public static void main(String[] args) {
        MemberService memberService = new MemberServiceImpl();

        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();

- OrderApp

public class OrderApp {
    public static void main(String[] args) {
        //MemberService memberService = new MemberServiceImpl();
        //OrderService orderService = new OrderServiceImpl();

        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();
        OrderService orderService = appConfig.orderService();
  • Test 코드
public class MemberServiceTest {

    //MemberService memberService = new MemberServiceImpl();

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

정상적으로 작동 확인

4. AppConfig 리팩토링

역할과 역할에 따른 구현이 명확하게 보이게 수정

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

  • AppConfig : 애플리케이션 사용 영역과, 객체를 생성하고 구성하는 영역으로 분리
  • 할인 정책 변경 시 AppConfig만 수정

6. AppConfig 수정 후 OrderApp 실행

public DiscountPolicy discountPolicy() {
        //return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
  • AppConfig : FixDiscountPolicy에서 RateDiscountPolicy 객체로 변경
  • 할인 정책이 변경되어도 구성 역할을 담당하는 AppConfig만 변경
    클라이언트 코드인 OrderServiceImpl를 포함해서 사용 영역의 코드 변경은 필요 없음

7. IoC, DI, 컨테이너

1) 제어의 역전 (IoC, Inversion of Control)

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

  • AppConfig가 프로그램의 흐름 제어, 구현 객체는 자신의 로직을 실행하는 역할만 담당
    (Impl은 필요한 인터페이스들을 호출, 어떤 객체들이 실행될지 알 필요가 없음)

  • 프로그램의 흐름을 직접 제어하는 것이 아니라, 외부에서 제어하는 것을 IoC라고 함

2) 의존관계 주입(DI, Dependency Injection)

  • 정적인 클래스 의존 관계와, 실행 시점에 결정되는 동적인 객체(인스턴스) 의존관계 둘을 분리해서 고려

  • 정적인 클래스 의존 관계
    정적인 의존관계는 애플리케이션을 실행하지 않고 분석이 가능
    OrderServiceImpl은 MemoryMemberRepository, DiscountPolicy에 의존하는 것을 확인 할 수 있음
    OrderServiceImpl에 어떤 객체가 주입 될 지는 알 수 없음

    • 클래스 다이어 그램
  • 동적인 객체 인스턴스 의존 관계
    : 애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계

    • 객체 다이어그램
  • 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고
    클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존 관계 주입(DI) 이라 함.

  • 객체 인스턴스를 생성, 참조된 값을 전달해서 연결

  • 의존관계 주입을 사용하면 클라이언트 코드 변경 없이, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경

  • 의존관계 주입을 사용하여 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경

참고
IoC 컨테이너, DI 컨테이너

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

8. 자바기반에서 스프링으로 전환

  • AppConfig에 @Configuration을 붙임
  • 각 메서드에 @Bean을 붙임 -> 스프링 컨테이너에 스프링 빈으로 등록
@Configuration
public class AppConfig {
    /*
    public MemberService memberService(){
        return new MemberServiceImpl(new MemoryMemberRepository());
    }

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


    @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) {
        //MemberService memberService = new MemberServiceImpl();

        //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("find member: " + findMember.getName());
    }
}
  • OrderApp 변경
public class OrderApp {
    public static void main(String[] args) {
        //MemberService memberService = new MemberServiceImpl();
        //OrderService orderService = new OrderServiceImpl();

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

        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 + " + order);
    }
}

참고
스프링 컨테이너

  • ApplicationContext: 스프링 컨테이너를 의미하며, 애플리케이션의 설정 및 객체 관리를 담당.
  • DI(Dependency Injection): 이전에는 개발자가 AppConfig를 사용해 직접 객체를 생성하고 DI를 수행했으나, 이제는 스프링 컨테이너를 통해 객체를 관리.
  • @Configuration: AppConfig 클래스에 붙여 설정 정보를 제공하며, 이 클래스 내의 @Bean 메서드를 호출하여 반환된 객체를 스프링 컨테이너에 등록.
  • 스프링 빈: @Bean이 붙은 메서드의 반환 객체를 스프링 빈이라 하며, 메서드 이름이 스프링 빈의 이름으로 사용됨 (예: memberService, orderService).
  • 객체 조회: 이전에는 개발자가 직접 객체를 조회했으나, 이제는 스프링 컨테이너를 통해 applicationContext.getBean() 메서드를 사용하여 필요한 스프링 빈을 찾아야 함.
  • 변경된 방식: 개발자가 직접 모든 것을 관리하던 방식에서, 스프링 컨테이너에 객체를 등록하고 이를 통해 스프링 빈을 찾아 사용하는 방식으로 변경됨.
profile
내 인생부터 개발

0개의 댓글