
이거 개웃기네
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;
}
}
}
Command + Shift + T를 눌러서 이 클래스에 대한 테스트 클래스를 생성할 수 있음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);
}
@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(1000);
}
}
테스트 실패 시 이와 같이 기대값과 실제 값을 표시해 줌
OrderServiceImpl 코드를 고쳐야 함public class OrderServiceImpl implements OrderService {
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
DiscountPolicyFixDiscountPolicy, RateDiscountPolicy
기대했던 의존 관계
DiscountPolicy 인터페이스만 의존한다고 생각했음
실제 의존 관계
DiscountPolicy 인터페이스 뿐만 아니라 FixDiscountPolicy 구체 클래스도 함께 의존하고 있음 (DIP 위반)FixDiscountPolicy를 RateDiscountPolicy로 변경하는 순간 OrderServiceImpl의 소스 코드도 함께 변경해야 함 (OCP 위반)OrderServiceImpl은 DiscountPolicy의 인터페이스 뿐만 아니라 구체 클래스도 함께 의존함public class OrderServiceImpl implements OrderService {
//private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private DiscountPolicy discountPolicy;
}
OrderServiceImpl에 DiscountPolicy의 구현 객체를 대신 생성하고 주입해주어야 함관심사를 분리하자
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는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성함MemberServiceImplMemoryMemberRepositoryOrderServiceImplFixDiscountPolicyAppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입(연결) 해줌MemberServiceImpl -> MemoryMemberRepositoryOrderServiceImpl -> MemoryMemberRepository, FixDiscountPolicypackage 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);
}
}
MemberServiceImpl은 MemoryMemberRepository를 의존하지 않음MemberRepository 인터페이스만 의존함MemberServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지) 알 수 없음MemberServiceImpl의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정됨MemberServiceImpl은 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 됨
클래스 다이어그램
AppConfig가 담당함MemberServiceImpl은 MemberRepository인 추상에만 의존하면 됨
회원 객체 인스턴스 다이어그램
appConfig 객체는 memoryMemberRepositry 객체를 생성하고 그 참조값을 memberServiceImpl을 생성하면서 생성자로 전달함memberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고 해서 DI(Dependency Injection), 의존관계 주입 또는 의존성 주입이라고 함package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;
public class OrderServiceImpl implements OrderService{
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 입장에서 생성자를 통해 어떤 구현 객체가 들어올지 알 수 없음OrderServiceImpl의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정함OrderServiceImpl은 실행에만 집중하면 됨package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
public class MemberApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.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());
}
}
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;
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, "MemberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 10000);
System.out.println("order = " + order);
System.out.println("order.calculatePrice() = " + order.calculatePrice());
}
}
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);
}
}
@BeforeEach는 테스트를 실행하기 전에 무조건 실행되는 구문package hello.core.order;
import hello.core.AppConfig;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
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() {
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);
}
}
기대하는 그림
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());
}
}
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();
}
}
new MemoryMemberRepository() 부분이 중복 제거 되었음. 이제 MemoryMemberRepository를 다른 구현체로 변경할 때 한 부분만 변경하면 됨AppConfig를 보면 역할과 구현 클래스가 한 눈에 들어오기 때문에 애플리케이션 전체 구성이 어떻게 되어있는지 빠르게 파악할 수 있음FixDiscountPolicy -> RateDiscountPolicy
사용, 구성의 분리
할인 정책의 변경
FixDiscountPolicy -> RateDiscountPolicy로 변경해도 구성 영역만 영향을 받고, 사용 영역은 전혀 영향을 받지 않음package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
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;
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();
}
}
AppConfig에서 할인 정책 역할을 담당하는 구현을 FixDiscountPolicy -> RateDiscountPolicy 객체로 변경함AppConfig만 변경하면 됨. 클라이언트 코드인 OrderServiceImpl을 포함해서 사용 영역의 어떤 코드도 변경할 필요가 없음DiscountPolicy 뿐만 아니라, 구체 클래스인 FixDiscountPolicy도 함께 의존 (DIP 위반)한 클래스는 하나의 책임만 가져야 한다.
프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다. 의존성 주입은 이 원칙을 따르는 방법 중 하나다.
OrderServiceImpl)는 DIP를 지키며, DiscountPolicy 추상화 인터페이스에 의존하는 듯 보였지만, FixDiscountPolicy 구체화 구현 클래스에도 함께 의존했음DiscountPolicy 추상화 인터페이스에만 의존하도록 코드를 변경함FixDiscountPolicy 객체 인스턴스를 클라이언트 코드 대신 생성해서 클라이언트 코드에 의존관계를 주입함. 이렇게해서 DIP 원칙을 따르면서 문제도 해결함소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
AppConfig가 의존관계를 FixDiscountPolicy -> RateDiscountPolicy로 변경해서 클라이언트 코드에 주입하므로 클라이언트 코드는 변경하지 않아도 됨OrderServiceImpl도 AppConfig가 생성함OrderServiceImpl이 아닌 OrderService 인터페이스의 다른 구현 객체를 생성하고 실행할 수도 있음OrderServiceImpl은 묵묵히 자신의 로직을 실행할 뿐임OrderServiceImpl은 DiscountPolicy 인터페이스에 의존함. 실제 어떤 구현 객체가 사용될지는 모름
클래스 다이어그램
OrderServiceImpl은 MemberRepository, DiscountPolicy에 의존한다는 것을 알 수 있음OrderServiceImpl에 주입되는지 알 수 없음
객체 다이어그램
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();
}
}
AppConfig에 설정을 구성한다는 뜻의 @Configuration을 붙여줌@Bean을 붙이면 스프링 컨테이너에 스프링 빈으로 등록함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());
}
}
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", 20000);
System.out.println("order = " + order);
System.out.println("order.calculatePrice() = " + order.calculatePrice());
}
}
ApplicationContext를 스프링 컨테이너라 함AppConfig를 사용해서 직접 객체를 생성하고 DI를 했지만 이제부터는 스프링 컨테이너를 통해 사용함@Configuration이 붙은 AppConfig를 설정 정보로 사용함@Bean이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록함@Bean이 붙은 메서드의 명을 스프링 빈의 이름으로 사용함 (memberService, orderService`)AppConfig를 사용해서 직접 조회했지만, 이제부터 스프링 컨테이너를 통해서 필요한 스프링 빈(객체)을 찾아야 함applicationContext.getBean() 메서드를 사용해서 찾을 수 있음