새로운 할인 정책 개발
- 새로운 할인 정책으로 확장
- 고정 금액 -> 10% 할인
//RateDiscountPolicy.java
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 ;
}else{
return 0;
}
}
}
- 테스트 작성
- class 선택한 후 command + shift + T : 테스트 클래스 만들어 줌
//RateDiscountPolicyTest.java
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;
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(2L, "memberVIP", Grade.BASIC);
//when
int discount = discountPolicy.discount(member, 10000);
//then
Assertions.assertThat(discount).isEqualTo(1000);
}
}
- 실패 테스트도 만들어 봐야함
- 실패 테스트 결과
//Assertions.assertThat(discount).isEqualTo(1000);
assertThat(discount).isEqualTo(1000);
- option+ enter : 상수나 enum을 사용할 때 해당 클래스명이나 enum명을 사용하지 않고 바로 변수를 사용할 수 있게 해줌
- 사용 이유 : 나중에 가면 갈수록 코드가 길어지면, 클래스 이름도 거추장스럽다
새로운 할인 정책 적용과 문제점
//OrderServiceImpl.java
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository=new MemoryMemberRepository();
//private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member,itemPrice);
return new Order(memberId,itemName,itemPrice,discountPrice);
}
}
- 할인 정책을 변경하려면 클라이언트인 OrderServiceImpl에서 FixDiscountPolicy() -> RateDiscountPolicy()로 코드를 고쳐야한다.
- 역활과 구현 충실하게 분리 O
- 다형성 활용, 인터페이스와 구현 객체 분리 O
- OCP,DIP 객체 지향 설계 원칙 준수 X
- 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있음. - > DIP 위반
- 추상 : DiscountPolicy
- 구체 : FixDiscountPolicy, RateDiscountPolicy
- 기능을 확장해서 변경하면 , 클라이언트 코드에 영향을 준다. -> OCP 위반
- 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있음. - > DIP 위반
//OrderServiceImpl.java
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository=new MemoryMemberRepository();
private DiscountPolicy discountPolicy; //이 부분 수정
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member,itemPrice);
return new Order(memberId,itemName,itemPrice,discountPrice);
}
}
- 인터페이스에만 의존하도록 코드 변경
- 구현체가 없음 -> null pointer exception (NPE) 발생
- 해결방안
- 누군가가 OrderServiceImpl에 DiscountPolicy 구현 객체를 대신 생성하고 주입해주어야 함
관심사의 분리
로미오와 줄리엣 공연으로 비유해보자.
로미오 역할, 줄리엣 역할 : 인터페이스
레오나르도 디카프리오: 구현체
앞선 상황은 디카프리오가 줄리엣 역할을 할 여자 주인공을 직접 초빙하는 것과 같다.
디카프리오는 공연 + 초빙 이라는 다양한 책임을 갖게 됨
-> 별도의 공연 기획자가 필요.
AppConfig 등장
- 구현 객체를 생성하고 연결하는 책임을 가지는 별도의 설정 클래스
//AppConfig.java
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는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입(연결)해준다.
//MemberServiceImpl.java
package hello.core.member;
public class MemberServiceImpl implements MemberService {
//private final MemberRepository memberRepository = new MemoryMemberRepository();
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);
}
}
- MemberServiceImpl이 MemoryMemberRepository에 의존하지 않게 됨
- MemberRepository라는 인터페이스에만 의존
- MemberServiceImpl의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정
- 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중할 수 있게 됨
- 클라이언트인 MemberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고 해서 DI(Dependency Injection) 의존관계 주입, 의존성 주입 이라고 한다.
//OrderServiceImpl.java
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
public class OrderServiceImpl implements OrderService{
//private final MemberRepository memberRepository=new MemoryMemberRepository();
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository,DiscountPolicy discountPolicy) {
this.memberRepository=memberRepository;
this.discountPolicy = discountPolicy;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member,itemPrice);
return new Order(memberId,itemName,itemPrice,discountPrice);
}
}
- OrderServiceImpl은 FixDiscountPolicy를 의존하지 않게됨
- DiscountPolicy 인터페이스에만 의존
- OrderServiceImpl의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정
- 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중할 수 있게 됨
AppConfig 실행
//MemberApp.java
package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
public class MemberApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig(); //추가
MemberService memberService = appConfig.memberService(); //추가
//MemberService memberService = new MemberServiceImpl();
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.java
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();
//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);
}
}
테스트 코드도 수정
//MemberServiceTest.java
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 = new MemberServiceImpl();
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); //join한거랑 찾은 거랑 같은가
}
}
//OrderServiceTest.java
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 = new MemberServiceImpl();
MemberService memberService;
// OrderService orderService = new OrderServiceImpl(discountPolicy);
OrderService orderService;
@BeforeEach
public void beforeEach(){
AppConfig appConfig =new AppConfig();
memberService = appConfig.memberService();
orderService =appConfig.orderService();
}
@Test
void createOrder(){
Long memberId =1L;
Member member = new Member(memberId,"memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId,"itemA",10000);
Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
}
}
- @BeforeEach는 각 테스트를 실행하기 전에 호출된다.
AppConfig 리팩토링
현재 AppConfig를 보면 중복이 있고 역할에 따른 구현이 잘 안보인다.
//AppConfig.java
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(memberRepository());
}
private static MemoryMemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(),discountPolicy());
}
public DiscountPolicy discountPolicy(){
return new FixDiscountPolicy();
}
}
- new MemoryMemberRepository() 중복 제거 ( 단축키 : command + option + m )
- 역할과 구현 클래스가 한눈에 들어옴
새로운 구조와 할인 정책 적용
정액 할인 정책 -> 정률 할인 정책
FixDiscountPolicy -> RateDiscountPolicy
- AppConfig의 등장으로 애플리케이션이 사용 영역과 , 객체를 생성하고 구성하는 영역으로 분리됨
- FixDiscountPolicy -> RateDiscountPolicy 로 변경해도 구성 영역만 영향을 받고, 사용 영역은 영향을 받지 않음
//AppConfig.java
package hello.core;
,,,
import hello.core.discount.RateDiscountPolicy;
,,,
public class AppConfig {
,,,
public DiscountPolicy discountPolicy(){
//return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
좋은 객체 지향 설계의 5가지 원칙의 적용
IoC, DI 그리고 컨테이너
제어의 역전 IoC
- 프로그램의 제어 흐름을 프로그램에서 관리하는게 아니라 외부에서 관리하는 것
- AppConfig가 프로그램의 제어 흐름을 가져감. 구현 객체는 자신의 로직을 실행하는 역할만 담당.
프레임워크 vs 라이브러리
- 내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크 (JUnit)
- 반면에 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 그것은 프레임워크가 아니라 라이브러리
의존관계 주입 DI (Dependency Injection)
- 인터페이스만 의존, 실제 어떤 구현 객체가 사용될지는 모른다.
- 정적인 클래스 의존관계
- 클래스가 사용하는 import 코드만 보고 의존관계를 쉽게 판단할 수 있다.
- 애플리케이션을 실행하지 않아도 분석 가능
- 이러한 클래스 의존관계 만으로는 실제 어떤 객체가 주입 될 지 알 수 없다.
- 동적인 객체 인스턴스 의존 관계
- 애플리케이션 실행 시점에 실제 생성된 객 체 인스턴스의 참조가 연결된 의존 관계
- 실행시점에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존 관계가 연결되는 것을 '의존관계 주입' 이라 한다.
- 의존 관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.
- 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.
IoC 컨테이너 , DI 컨테이너
- AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것
- 의존관계 주입에 초점을 맞추어 최근에는 주로 DI 컨테이너라 한다.
- 또는 어셈블러, 오브젝트 팩토리 등으로 불리기도 한다.
스프링으로 전환하기
//AppConfig.java
package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
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 MemoryMemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(),discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy(){
//return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
- @Configuration : 설정 정보를 구성
- 각 메소드에 @Bean : 스프링 빈에 등록
//MemberApp.java
package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebApplicationContext;
import org.springframework.context.ApplicationContext;
public class MemberApp {
public static void main(String[] args) {
// AppConfig appConfig = new AppConfig();
// MemberService memberService = appConfig.memberService();
ApplicationContext applicationContext = new AnnotationConfigReactiveWebApplicationContext(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());
}
}
- ApplicationContext는 스프링 컨테이너 : @Bean 객체들 관리
- AppConfig.class : AppConfig에 있는 환경설정 정보들을 관리
- 상위 다섯개는 스프링 내부적으로 필요해서 등록하는 스프링빈
- 아래 다섯개는 @Bean으로 AppConfig에 작성해둔 것들
//OrderApp.java
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;
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebApplicationContext;
import org.springframework.context.ApplicationContext;
public class OrderApp {
public static void main(String[] args) {
//AppConfig appConfig = new AppConfig();
//MemberService memberService = appConfig.memberService();
//OrderService orderService = appConfig.orderService();
ApplicationContext applicationContext =new AnnotationConfigReactiveWebApplicationContext(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);
}
}
- 스프링 빈은 @Bean이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다.
- @Bean(name ="")으로 변경할 수도 있음 : 특별한 경우가 아니면 사용 X
- 스프링빈은 applicationContext.getBean() 메서드를 사용해서 찾을 수 있다.
- 이제부터는 스프링 컨테이너에 객체를 스프링 빈으로 등록하고, 스프링 컨테이너에서 스프링 빈을 찾아서 사용하도록 변경
'개발 스터디 > Spring' 카테고리의 다른 글
[스프링 핵심 원리 - 기본편] 컴포넌트 스캔 (1) | 2023.05.16 |
---|---|
[스프링 핵심 원리 - 기본편] 싱글톤 컨테이너 (0) | 2023.05.14 |
[스프링 핵심 원리 - 기본편] 스프링 핵심 원리 이해1 - 예제 만들기 (0) | 2023.05.02 |
[스프링 핵심원리 - 기본편] 객체 지향 설계와 스프링 (0) | 2023.05.02 |
[Spring 스터디]웹 MVC 개발 (0) | 2023.04.08 |