스프링 핵심 원리 이해 2

유병익·2022년 10월 17일
0

스프링 핵심 원리

목록 보기
3/9
post-thumbnail

1. 새로운 할인 정책 개발


1.1 새로운 할인 정책

  • 정액 할인 → 정률 할인

💡 우리는 기존의 DiscountPolicy 인터페이스를 활용해 새로운 할인 정책(정률 할인) 구현체를 만들면 된다!

1.1.1 RateDiscountPolicy Code

package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;

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

1.1.2 RateDiscountPolicy Test Code

package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;

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

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


💡 할인 정책을 변경하려면 클라이언트인 OrderServiceImpl 코드를 고쳐야 한다.
💡 지금 코드는 기능을 확장해서 변경하면, 클라이언트 코드에 영향을 준다 (OCP위반)

1.2.1 기대했던 의존 관계

1.2.2 실제 의존 관계

💡 OrderServiceImplDiscountPolicy 인터페이스와 FixDiscountPolicy 모두 의존 → DIP 위반

💡 FixDiscountPolicy를 RateDiscountPolicy로 변경하면 OrderServiceImpl의 소스 코드도 변경해야함 → OCP 위반


1.2.3 해결 방법

  • DIP를 위반하지 않도록 인터페이스에만 의존하도록 의존 관계를 변경
  • 인터페이스에만 의존하도록 설계를 변경
public class OrderServiceImpl implements OrderService {
 //private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
		 private DiscountPolicy discountPolicy;
}

❓ 그런데 구현체가 없는데 어떻게 코드를 실행할 수 있을까?


❗ 누군가가 클라이언트인 OrderServiceImplDiscountPolicy의 구현 객체를 대신 생성하고 주입해주어야 한다.

2. AppConfig의 등장


2.1 AppConfig

  • 애플리케이션의 전체 동작 방식을 구성하기 위해, 구현 객체를 생성하고, 연결하는 별도의 설정 클래스
  • AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
  • AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입(연결)해준다.

MemberServiceImplMemoryMemberRepository 의존 X

MemberRepository 인터페이스만 의존

생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정

의존관계에 대한 고민은 외부에 맡기고 실행에만 집중

💡 추상(Interface)에만 의존 → DIP 완성

💡 객체를 생성하고 연결하는 역할과 실행하는 역할이 명확히 분리 → 관심사의 분리


2.2 AppConfig Refactoring

2.2.1 AppConfig.java (Before Refactoring)

package hello.core;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

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

		 public OrderService orderService() {
				 return new OrderServiceImpl(new MemoryMemberRepository(),new FixDiscountPolicy());
		 }
}
  • 이전 AppConfig를 보면 중복이 있고, 역할에 따른 구현이 잘 안보인다
  • 중복을 제거하고, 역할에 따른 구현이 보이도록 리팩터링

2.2.2 AppConfig.java (After Refactoring)

package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

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();
		 }
}
  • 애플리케이션 전체 구성이 어떻게 되어있는지 빠르게 파악 가능

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

3. IoC, DI, 그리고 컨테이너


3.1 제어의 역전 IoC(Inversion of Control)

  • 기존 프로그램은 구현 객체가 프로그램의 제어 흐름을 스스로 조종
  • AppConfig 등장 이후, 구현 객체는 자신의 로직을 실행하는 역할만 담당
  • 프로그램에 대한 제어 흐름에 대한 권한은 모두 AppConfig가 가지고 있다.
  • 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC))이라 한다.

3.2 Framework vs Library

  • 내가 작성한 코드를 제어하고, 대신 실행하면 Framework
  • 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 Library

3.3 의존관계 주입 DI(Dependency Injection)

  • 정적인 클래스 의존 관계와, 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계를 분리해서 생각해야 함

    3.3.1 정적인 클래스 의존관계

    • 클래스가 사용하는 import 코드만 보고 의존관계를 쉽게 판단할 수 있다.

    • 정적인 의존관계는 애플리케이션을 실행하지 않아도 분석할 수 있다

      3.3.2 동적인 객체 인스턴스 의존 관계

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

    • 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성 및 클라이언트에 전달

    • 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라 한다.

    • 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.

  • AppConfig처럼 객체를 생성 및 관리 & 의존관계 연결 → IoC 컨테이너 or DI 컨테이너

4. 스프링으로 전환


4.1 AppConfig 스프링 기반으로 변경

package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

4.2 MemberApp에 스프링 컨테이너 적용

package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class MemberApp {
		 public static void main(String[] args) {
				 // 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()); 
		 }
}

4.3 OrderApp에 스프링 컨테이너 적용

package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.order.Order;
import hello.core.order.OrderService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class OrderApp {
		 public static void main(String[] args) {
				 // AppConfig appConfig = new AppConfig();
				 // MemberService memberService = appConfig.memberService();
				 // OrderService orderService = appConfig.orderService();
				 ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
				 MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
				 OrderService orderService = applicationContext.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 = " + order);
		 }
}

  • ApplicationContext스프링 컨테이너라 한다.
  • 스프링 컨테이너는 @Configuration 이 붙은 AppConfig를 설정(구성) 정보로 사용한다.
  • @Bean 이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다.
  • 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다.
  • 스프링 빈은 applicationContext.getBean() 메서드를 사용해서 찾을 수 있다.
💡 코드가 약간 더 복잡해진 것 같은데, 스프링 컨테이너를 사용하면 어떤 장점이 있을까?
profile
Backend 개발자가 되고 싶은

0개의 댓글