[Spring 기본] 2. 객체지향 원리 적용

heyhey·2023년 1월 30일
0

Spring

목록 보기
10/23

[Spring 기본] 2. 객체지향 원리 적용

할인 정책이 변경되었다. 고정 금액이 아닌 금액당 할인하는 방법으로 변경하고 싶다.

RateDiscountPolicy 추가

DiscountPolicy 인터페이스를 구현한 RateDiscountPolicy를 추가해준다.

public class RateDiscountPolicy implements DiscountPolicy {
  private int discountPercent = 10; //10% 할인

  @Override
  public int discount(Member member, int price) {
    if (member.getGrade() == Grade.VIP) {
      return price * discountPercent /  100;
    } else {
        return 0;
    }
  }
}

RateDiscountPolicy Test

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

테스트는 잘 작동한다. 이 할인 정책을 애플리케이션에서 적용하고 싶다.

주문 서비스 구현체

앞 장에서 만들었던 구현체를 다시 수정해줘야한다.

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

문제점
역할과 구현을 잘 구현하였다. 다형성을 활용하고 인터페이스와 구현 객체도 분리하였다.
OCP 위반 : 현재 코드는 기능을 확장해서 변경하면, 클라이언트 코드에 영향을 주게 된다.
DIP 위반 : 클래스 의존관계를 분석해보면, 추상 뿐만 아니라 구현 클래스에도 의존하고 있다.
-> RateDiscountPolicyFixDiscountPolicy 가 아닌 DiscountPolicy 인터페이스에 의존해야한다.

어떻게 문제를 해결해야 할까?

인터페이스에만 의존하도록 코드를 변경한다.

public class OrderServiceImpl implements OrderService {
  private final MemberRepository memberRepository;
  private final DiscountPolicy discountPolicy;
  ...
}

구현체가 없어서 null pointer exception이 발생한다.
=> OrderServiceImplDiscountPolicy의 구현 객체를 대신 생성하고 주입해줄 뭔가가 필요하다.

관심사의 분리

책임을 분산시켜라. 객체를 생성하고 연결하는 역할과 실행하는 역할을 분리하자.
구현 객체를 생성하고 연결하는 책임을 가지는 뭔가가를 만들어준다.

AppConfig

AppConfig에서는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
생성한 객체 인스턴스의 참조를 생성자를 통해서 주입해준다.

public class AppConfig {
  public MemberService memberService() {
    return new MemberServiceImpl(new MemoryMemberRepository());
  }
  public OrderService orderService() {
    return new OrderServiceImpl(new MemoryMemberRepository(),new FixDiscountPolicy());
  }
}

MemberServiceImpl 생성자 주입

생성자를 만들어준다.

public class MemberServiceImpl implements MemberService {
  private final MemberRepository memberRepository;

  public MemberServiceImpl(MemberRepository memberRepository) {
    this.memberRepository = memberRepository;
  }
}
  • 설계 변경으로 MemberServiceImpl는 더이상 MemoryMemberRepository를 의존하지 않는다.
    => MemberRepository 인터페이스를 의존한다.
  • MemberServiceImpl의 생성자를 통해서 어떤 구현 객체를 주입하는지는 AppConfig에서 결정된다.
  • MemberServiceImpl의 의존관계는 외부에 맡기고 실행에만 집중한다.

DI

Dependency Injection 로 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라고 한다.

OrderServiceImpl 생성자 주입

public class OrderServiceImpl implements OrderService {
  public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
  }
}

AppConfig 실행

앞서 만들었던 Test 코드에서 사용하기 위해서는 AppConfig를 선언하고 해당 서비스를 사용하면 된다.

MemberService

class MemberServiceTest {
  MemberService memberService;
  @BeforeEach
  public void beforeEach() {
    AppConfig appConfig = new AppConfig();
    memberService = appConfig.memberService();
  }
}

할인 정책의 변경

FixDiscountPolicy => RateDiscountPolicy 로 변경하려고 한다.
이제는 AppConfig에서 DiscountPolicy에 대한 구현만 변경해준다.
아래는 한번 중복을 제거하고 조금더 명시적으로 리팩터링했다.

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

이제는 할인 정책을 변경해도,클라이언트 코드인 OrderServiceImpl를 변경하지 않고 애플리케이션의 구성 역할을 담당하는 AppConfig만 변경하면 된다.

스프링으로 전환하기


지금까지 순수한 자바 코드만을 이용해 AppConfig을 생성하고, DI를 적용했다.
이 AppConfig를 DI 컨테이너라고도 부른다.
이제 이 코드를 스프링으로 전환해보자

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

@Configuration@Bean이 추가되었다.

  • @Configuration : AppConfig에 설정을 구성한다.
  • @Bean : 스프링 컨테이너에 스프링 빈으로 등록한다.

사용할 때 조금은 복잡해 보일 수 있다.

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
  • ApplicationContext 를 스프링 컨테이너라고 부른다.
  • 기존에는 AppConfig를 이용해 직접 객체를 생성하고 DI를 했지만, 지금부터는 스프링을 이용한다.
  • 스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정 정보로 사용한다.
  • @Bean 이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다.
  • 이전에는 개발자가 필요한 객체를 AppConfig를 사용해서 조회했지만, 이제부터는 스프링 컨테이너를 통해 빈을 찾는다 (getBean)

코드가 더 복잡해진 것 같은데 다음장에서 스프링의 장점을 찾아보자

profile
주경야독

0개의 댓글