3.1 새로운 할인 정책 개발
새로운 할인 정책
- 기획자가 서비스 오픈 직전에 할인 정책을 정률제로 바꾸고 싶다고 하면 어떻게 해야할까?
Ex)
- RateDiscountPolicy 클래스를 만든다.
<hide/>
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;
}
}
- 테스트 클래스 만들기
<hide/>
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;
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
Assertions.assertThat(discount).isEqualTo(1000);
}
}
Note) 실행 결과 - 성공 케이스
Ex) 실패 케이스
<hide/>
@Test
@DisplayName("VIP가 아니면 할인이 적용되지 않아야한다.")
void vip_x(){
// given
Member member = new Member(2L, "memberBASIC", Grade.BASIC);
// when
int discount = discountPolicy.discount(member, 10000);
// then
Assertions.assertThat(discount).isEqualTo(0);
}
Note) 실행 결과 - 실패 케이스
3.2 새로운 할인 정책과 문재점
- 이제 OrderServiceImpl 클래스의 내용 중, FixDiscountPolicy => RateDiscountPolicy로 바꾸면 된다.
- OCP, DIP 같은 객체지향 설계 원칙을 준수하지 못했다
-> DIP: 추상 클래스 뿐만아니라 구체(구현) 클래스에도 의존하고 있다.
-> OCP: 현재 코드는 기능을 확장해서 변경하면 클라이언트 코드에 영향을 준다. => OCP 위반
- 정액제를 정률제로 변경하는 순간 OrderServiceImpl의 소스 코드도 함께 변경해야한다. => "OCP"위반
- 그러면 인터페이스에만 의존하도록 다음과 같이 코드를 변경하면?
-> NullPointerException이 발생한다. => 선언만 하고 생성을 하지 않았기 때문이다.
<hide/>
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository = new MemoryMemberRepository();
// private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
...
Note) 해결 방안
- 누군가가 클라이언트인 "OrderServiceImpl"에 "DiscountPolicy"의 구현 객체를 대신 생성하고 주입해줘야한다.
3.3 관심사의 분리
- AppConfig클래스 만든다. 애플리케이션 전체에 대해 환경 설정하고 구성한다는 뜻이다.
-> AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
-> AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입(연결) 해준다. => 'injection'
-> MemberServiceImpl은 MemoryMemberRepository에서 쓴다.
- MemberServiceImpl 클래스를 다음과 같이 수정한다.
-> MemberRepository변수를 선언만하고 생성은 하지 않는다.
-> 클래스의 생성자를 만든다. 생성자를 통해 MemberRepository에 뭐를 넣을지 정할 예정이다.
-> 추상화에만 의존하도록 만든다.
<hide/>
package hello.core.member;
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
- AppConfig
-> MemberService를 호출하면 new MemberServiceImpl(new MemoryMemberRepository)가 생성된다.
<hide/>
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());
}
}
- OrderServiceImpl 클래스 내용을 수정한다. (impl은 기능을 실행하는 부분만 책임진다)
-> final은 무조건 생성 또는 생성자를 통해서 할당되어야한다.
-> OrderServiceImpl의 멤버변수 memberRepository와 discountPolicy 를 선언하기만 하면 오류 난다.
-> 따라서, 선언한 다음에 OrderServiceImpl 의 생성자를 만들어주면 오류가 해결된다.
- OrderApp 클래스 수정
<hide/>
package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.order.Order;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
public class OrderApp {
public static void main(String[] args) {
MemberService memberService = new MemberServiceImpl(null);
OrderService orderService = new OrderServiceImpl(null, null);
Long memberId = 1L;
Member member = new Member(memberId, "itemA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 10000);
System.out.println("order = " + order.toString());
System.out.println("order.calculatePrice = " + order.calculatePrice());
}
}
Note) 실행 결과
Ex) 위의 코드의 null 부분을 AppConfig를 쓰도록 바꾼다.
<hide/>
package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.order.Order;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
public class OrderApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
OrderService orderService =appConfig.orderService();
Long memberId = 1L;
Member member = new Member(memberId, "itemA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 10000);
System.out.println("order = " + order.toString());
System.out.println("order.calculatePrice = " + order.calculatePrice());
}
}
Note) 실행 결과
- OrderApp은 더이상 구체 클래스에 의존하지 않는다. 인터페이스에만 의존할 뿐이다.
- MemberServiceTest 클래스도 수정
-> AppConfig를 사용하도록 한다.
-> @BeforeEach: 각 테스트 전에 무조건 실행되는 부분이다. 테스트 개수 만큼 돌아간다.
<hide/>
package hello.core.member;
import hello.core.AppConfig;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class MemberServiceTest {
MemberService memberService;
@BeforeEach
public void beforeEach(){
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);
}
}
- OrderServiceTest 클래스도 수정한다.
<hide/>
package hello.core.order;
import hello.core.AppConfig;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class OrderServiceTest {
MemberService memberService;
OrderService orderService;
@BeforeEach
public void beforeEach(){
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
}
@Test
void createOrder(){
// given
Long memberId = null;
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 10000);
Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
}
}
Note) 실행결과
- 이제 AppConfig를 통해 관심을 확실하게 분리했다.
- AppConfig는 공연기획자와 같은 역할이다.
3.4 AppConfig 리팩토링
- 그런데 현재 AppConfig를 보면 중복이 있고 역할에 따른 구현이 잘 안 보인다는 문제가 있다.
- AppConfig 클래스 - 구성 정보
-> 메서드명에 따라 역할과 구현 클래스가 명확하게 보인다.
=============코드 내용이 나중에 바뀔 예정 ===============
<hide/>
package hello.core;
import hello.core.discount.DiscountPolicy;
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(memoryMemberRepository());
}
private MemoryMemberRepository memoryMemberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService(){ // 누군가 오더서비스를 조회하면?
return new OrderServiceImpl( memoryMemberRepository(), discountPolicy());
}
public DiscountPolicy discountPolicy(){
return new FixDiscountPolicy();
}
}
3.5 새로운 구조와 할인 정책 적용
Ex) 정액제 => 정률제
- 할인 정책을 바꾸면 AppConfig의 discountPolicy 의 반환값만 RateDiscountPolicy로 바꾸면 된다.
- OrderServiceImpl은 바꿀 필요가 없다.
- OrderApp 클래스 수정
<hide/>
package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.order.Order;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
public class OrderApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
OrderService orderService =appConfig.orderService();
Long memberId = 1L;
Member member = new Member(memberId, "itemA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 20000);
System.out.println("order = " + order.toString());
}
}
Note) 실행결과
3.6 전체 흐름 정리
- 새로운 할인 정책 개발
- 새로운 할인 정책 적용과 문제점 =>클라이언트가 DiscountPolicy 뿐만 아니라 'FixDisountPolicy'에도 의존 => 'DIP'위반
- 관심사의 분리 => 'AppConfig'
- AppConfig 리팩토링 - new를 하나만 나오도록한다(중복 제거). 역할과 구현을 분리
- 새로운 구조와 할인 정책 적용
3.7 좋은 객체 지향 설계의 5가지 원칙의 적용
SRP - 단일 책임 원칙(1 클래스 - 1 책임)
- 구현 객체를 생성하고 연결하는 책임은 AppConfig가 담당한다.
- 클라이언트 객체를 실행하는 역할만 담당
DIP - 의존관계 역전 원칙(개발자는 구체화가 아닌 추상화에 의존하도록)
- AppConfig가 'FixDiscountPolicy' 객체 인스턴스를 클라이언트 코드대신 생성해서 클라이언트 코드에 의존 관계를 주입해서 DIP를 지킬 수 있다.
OCP - 개방 폐쇄 원칙
- 애플리케이션을 사용 / 구성 영역으로 나눈다.
- 소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀 있어야 한다.
3.8 IoC, DI, 컨테이너
IoC(Inversion Of Control) - 제어의 역전
- 기존의 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성, 연결, 실행했다. 즉, 구현 객체가 프로그램의 제어 흐름을 스스로 컨트롤했다.
- 반면에, AppConfig를 사용하면 구현 객체는 자신의 로직을 실행하는 역할만 담당한다.
- ex) 프로그램을 제어하는 권한은 모두 AppConfig가 가지고 있다. OrderServiceImpl 또한 AppConfig가 생성한다. 그리고 AppConfig는 OrderServiceImpl 가 아닌 OrderService 인터페이스의 다른 구현 객체를 생성하고 실행할 수 있다. 그런 사실과 무관하게 OrderServiceImpl는 자신의 로직을 실행한다.
- 이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전이라고 한다.
DI(Dependency Injection) - 의존 관계 주입
- OrderServiceImpl은 DiscountPolicy 인터페이스에 의존한다. 실제 어떤 구현 객체가 사용될지 모른다.
- 의존 관계는 정적인 클래스 의존 관계와 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계 둘을 분리해서 생각해야한다.
- 정적인 클래스 의존 관계: import문만 봐도 분석 가능
- 동적인 객체(인스턴스) 의존 관계: 애플리케이션을 실행하고나서 분석 가능하다.
프레임워크(Framework) vs 라이브러리(Library)
- 프레임워크가 내 코드를 제어하고 대신 실행하면 프레임워크가 맞다. ex) JUnit
- 내 코드가 제어의 흐름을 직접 담당하는 경우는 라이브러리
객체 다이어그램
- 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존 관계가 되는 것을 의존관계 주입이라고 한다.
- 객체 인스턴스를 생성하고 그 참조값을 전달해서 연결된다.
- 의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고 클라이언트가 호출하는 대상의 타입 인스턴스를 변경 가능하다.
IoC 컨테이너, DI 컨테이너
- AppConfig 처럼 객체를 생성하고 관리하면서 의존 관계를 연결해주는 것을 'IoC 컨테이너' 또는 'DI 컨테이너' 라고 한다.
- 최근에는 주로 'DI 컨테이너'라고 많이 한다.
3.9 스프링으로 전환하기
Ex) 지금까지 자바 프로젝트를 만든 것과는 다르게 스프링을 사용해보자
- AppConfig 클래스에 @Configuration어노테이션을 붙인다. => 애플리케이션의 설정 정보를 담당한다.
-> AppConfig 하위의 각 메서드에 @Bean을 붙인다.
-> @Bean을 붙이면 그 메서드들이 모두 스프링 컨테이너에 모두 빈으로 등록된다. 기본적으로 메서드 이름(key)으로 등록된다. 메서드 반환값은 value이다.
-> 또한, @Bean(name="메서드명 대신 붙이고 싶은 이름")을 이용해서 객체의 이름을 정할 수도 있다.
<hide/>
package hello.core;
import hello.core.discount.DiscountPolicy;
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;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memoryMemberRepository());
}
@Bean
public MemoryMemberRepository memoryMemberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService(){ // 누군가 오더서비스를 조회하면?
return new OrderServiceImpl( memoryMemberRepository(), discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy(){
return new FixDiscountPolicy();
}
}
- MmberApp
-> ApplicationContext는 스프링 컨테이너라고 볼 수 있다. bean과 같은 객체들을 모두 관리한다.
-> new AnnotationConfigApplicationContext(AppConfig.class)
=> 클래스 AppConfig 안에 있는 환경 설정 정보를 가지고 스프링이 모든 빈을 스프링 컨테이너에 넣어서 관리해준다.
-> 이제, AppConfig가 아닌 ApplicationContext에서 빈을 찾아와야 한다.
=> appliacationContext.getBean("memberService", MemberService.class); ..("이름", "반환 타입")
=> 스프링 컨테이너 appliacationContext에서 이름이 "memberService"인 객체를 찾아서 MemberService 형태로 반환하겠다는 뜻이다.
<hide/>
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); // memberService를 찾겠다
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());
}
}
Ex) OrderApp으로 실습
<hide/>
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, "itemA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 20000);
System.out.println("order = " + order.toString());
}
}
Note) 실행 결과
스프링 컨테이너(Stpring Container)
- ApplicationContext를 스프링 컨테이너라고 한다.
- 기존에는 AppConfig를 사용해서 직접 객체를 생성하고 의존 관계 주입을 했지만 이제는 스프링 컨테이너를 사용한다.
- @Configuration이 붙은 'AppConfig'를 설정(구성) 정보로 사용한다. Bean(@Bean이라고 적힌 객체)을 모두 호출해서 반환된 객체를 컨테이너에 등록한다.
'Spring Framework > [인프런] Spring 핵심 원리' 카테고리의 다른 글
Chapter 06. 컴포넌트 스캔(@ComponentScan) (0) | 2022.08.14 |
---|---|
Chapter 05. 싱글톤 컨테이너 (0) | 2022.08.14 |
Chapter 04. 스프링 컨테이너와 스프링 빈 (0) | 2022.08.12 |
Chapter 02. 스프링 핵심 원리 이해1 - 예제 만들기 (0) | 2022.08.11 |
Chapter 01. 객체 지향 설계와 스프링 (0) | 2022.08.11 |