package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
public class RateDiscountPolicy implements DiscountPolicy{
private int discountRate = 10;
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return price * discountRate / 100;
} else {
return 0;
}
}
}
이 정률 할인 정책이 잘 적용이 되는지를 테스트하기
package hello.core.discount;
import hello.core.member.*;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class RateDiscountPolicyTest {
RateDiscountPolicy rateDiscountPolicy = new RateDiscountPolicy();
@Test
@DisplayName("VIP는 10% 할인 적용")
void discount() {
Member member = new Member(1L, "memberA", Grade.VIP);
int discount = rateDiscountPolicy.discount(member, 10000);
Assertions.assertThat(discount).isEqualTo(1000);
}
@Test
@DisplayName("VIP가 아니면 할인 미적용")
void vip_x() {
Member member = new Member(1L, "memberA", Grade.BASIC);
int discount = rateDiscountPolicy.discount(member, 10000);
Assertions.assertThat(discount).isEqualTo(0);
}
}
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 findMember = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(findMember, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
주석으로 처리된 부분은 정액 할인 정책, 그 아래 코드는 정률 할인 정책이다.
할인 정책을 변경하면 클라이언트인 OrderServiceImpl
코드를 수정해야하는 문제가 발생한다.
AppConfig
는 애플리케이션 실제 동작에 필요한 구현 객체를 생성한다.
AppConfig
는 생성한 객체 인스턴스 참조를 생성자를 통해서 주입한다.
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;
public class AppConfig {
// 각각의 역할이 명확하게 드러나도록 작성하는 것이 좋음
public MemberService memberService() {
// ❌️return new MemberServiceImpl(new MemberRepository()); 중복되므로 지양
return new MemberServiceImpl(memberRepository());
}
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService() {
// ❌️return new OrderServiceImpl(new MemberRepository(), new DiscountPolicy());
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
AppConfig는 구현체 클래스를 선택한다. 애플리케이션이 어떻게 동작해야 할지 전체 구성을 책임진다.
각각의 역할을 구현한 구현체 클래스는 기능을 실행하는 책임만 지면 된다.
MemberServiceImpl
수정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 id) {
return memberRepository.findById(id);
}
}
핵심 코드
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
MemberServiceImpl
은 MemoryMemberRepository
에 직접적으로 의존하지 않는다.
MemberRepository
라는 인터페이스(역할)에 의존한다.
MemberServiceImpl
입장에서 의존관계를 마치 외부에서 주입하는 것 같다고 해서 DI(Dependency Injection), 우리말로 의존관계 주입 또는 의존성 주입이라고 한다.
OrderServiceImpl
수정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;
//private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
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 findMember = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(findMember, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
핵심 코드
private final MemberRepository memberRepository;
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
OrderServiceImpl
은 DiscountPolicy 인터페이스(역할)에 의존한다.
OrderServiceImpl
입장에서 의존관계를 마치 외부에서 주입하는 것 같다고 해서 DI(Dependency Injection), 우리말로 의존관계 주입 또는 의존성 주입이라고 한다.
MemberServiceTest
수정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 {
private MemberService memberService;
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
}
@Test
void join() {
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
Member findMember = memberService.findMember(member.getId());
Assertions.assertThat(member.getId()).isEqualTo(findMember.getId());
}
}
OrderServiceTest
수정package hello.core.order;
import hello.core.AppConfig;
import hello.core.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 {
private MemberService memberService;
private OrderService orderService;
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
}
@Test
void createOrder() {
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(member.getId(), "itemA", 10000);
Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
}
}
AppConfig
가 전체 애플리케이션의 실행 동작을 결정하기 때문에 새로운 할인 정책을 적용하고싶다면 AppConfig
의 DiscountPolicy 역할 부분을 수정하면 된다.
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 MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy(); // 정률제
//return new FixDiscountPolicy(); // 정액제
}
}
새로운 할인 정책 개발
새로운 할인 정책 적용 문제점
관심사의 분리
AppConfig
(공연 기획자)가 등장AppConfig
의 역할과 구현을 명확하게 분리(역할 잘 돋보이도록 & 중복 제거)새로운 구조의 할인 정책 적용
AppConfig
의 할인 정책을 담당하는 역할 쪽에서 구현체 클래스의 반환만을 달리하여 적용SRP : 클라이언트 객체는 실행만을 담당 & AppConfig
는 전체 애플리케이션의 실행 동작만을 담당
DIP : 구체화에 의존하지 않고 추상화에 의존
→ 구현체 클래스(구체화X), 인터페이스(추상화O)
OCP : AppConfig가 의존관계를 주입하여 클라이언트 코드 변경 불필요
→ 소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀 있다.
제어의 역전(IoC, Inversion of Control)
AppConfig
가 등장한 이후부터 구현 객체는 자신의 로직을 실행하는 역할만을 담당한다.
프로그램의 제어 흐름은 AppConfig
가 쥐고 있다. OrderServiceImpl
이나 MemberServiceImpl
입장에서 보면 생성자를 통해 어떤 구현 객체들이 실행될지 모른다.
프로그램의 제어 흐름을 외부에서 관리하는 것을 제어의 역전이라고 한다.
의존관계 주입(DI, Dependency Injection)
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
OrderServiceImpl
은 DiscountPolicy
인터페이스에 의존한다. 실제 어떤 구현 객체가 사용될진 모른다.
IoC 컨테이너, DI컨테이너
AppConfig
처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것을 IoC컨테이너 또는 DI컨테이너라고 한다.
의존관계 주입에 초점을 맞춰 최근에는 주로 DI컨테이너라고 불린다.
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;
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 MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean // 스프링 빈 어노테이션
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean // 스프링 빈 어노테이션
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
//return new FixDiscountPolicy();
}
}
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.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 ac = new AnnotationConfigApplicationContext(AppConfig.class);
// 스프링 빈 조회
MemberService memberService = ac.getBean("memberService", MemberService.class);
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
Member findMember = memberService.findMember(1L);
System.out.println("findMember = " + findMember.getName());
System.out.println("member = " + member.getName());
}
}
package hello.core;
import hello.core.order.Order;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import hello.core.member.*;
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 ac = new AnnotationConfigApplicationContext(AppConfig.class);
// 스프링 빈 조회
MemberService memberService = ac.getBean("memberService", MemberService.class);
OrderService orderService = ac.getBean("orderService", OrderService.class);
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(1L, "itemA", 10000);
System.out.println("order = " + order);
}
}
❓스프링 컨테이너
ApplicationContext
를 스프링 컨테이너라고 한다.
기존에는 AppConfig
를 통해서 직접 객체를 생성하고 DI했지만, 이제부터는 스프링 컨테이너를 통해서 사용한다.
스프링 컨테이너는 @Configuration
어노테이션이 붙은 AppConfig
를 설정 정보로 사용한다.
또한 이 어노테이션이 붙은 AppConfig
내에 @Bean
어노테이션이 붙은 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라고 한다.
스프링 빈은 @Bean
어노테이션이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다. 스프링 컨테이너를 통해서 필요한 스프링 빈을 찾아야 하는데 스프링 빈은 applicationContext.getBean()
메서드를 통해서 찾을 수 있다.