할인정책을 정액할인에서 “정률할인”으로 변경하기로 함.
=> 애자일 소프트웨어 개발 선언. https://agilemanifesto.org/iso/ko/manifesto.html
공정과 도구보다 개인과 상호작용을
포괄적인 문서보다 작동하는 소프트웨어를
계약 협상보다 고객과의 협력을
계획을 따르기보다 변화에 대응하기를
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;
}
}
}
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.assertj.core.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가 아니면 10%할인이 적용되지 않아야 한다.")
void vip_x() {
// given
Member member = new Member(1L, "memberBasic", Grade.BASIC);
// when
int discount = discountPolicy.discount(member, 10000);
// then
assertThat(discount).isEqualTo(0);
}
}
alt+enter
Assertions는 add on-demand static import for'~'해주는게 좋다.
그럼 코드가 많이 단축됨.
영한님이 10% 할인하는 코드도 굉장히 많은 테스트가 필요하다고 하셨다. 지금 굉장히 코드가 잘 짜여있어 테스트도 쉬운 것이라고. 배우는 입장에서 실감이 안 될수도 있지만 지금 테스트가 굉장히 잘 이루어진 것이라고. 아직 잘 모르는 내 눈에도 어떤 느낌인지 막연히 와닿았다. 지금 코드가 단순히 학습을 위해 최소한으로 짜여졌음에도 꽤나 양이 된다. 코드가 방대해지고 복잡해지면 테스트가 얼마나 어려울지.. 바로바로 테스트하고 최소한의 단위로 테스트를 시험해보는 것이 너무 중요할 것 같다.
할인 정책을 애플리케이션에 적용해보자.
OrderServiceImpl
코드를 수정했다.public class OrderServiceImpl implements OrderService {
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
기대했던 의존관계
실제 의존관계 & 정책 변경
📢 애플리케이션을 하나의 공연이라 생각해보자. 각각의 인터페이스를 배역(배우 역할)이라 생각하자. 그런데! 실제 배역을 맡을 배우를 선택하는 것은 누가 하는가?
✔️ 로미오와 줄리엣 공연을 하면 로미오 역할을 누가 할지 줄리엣 역할을 할지는 배우들이 정하는게 아니다. 이전 코드는 마치 로미오 역할(인터페이스)을 하는 레오나르도 데카프리오(구현체, 배우)가 줄리엣 역할(인터페이스)을 하는 여자 주인공(구현체, 배우)을 직접 초빙하는 것과 같다. 디카프리오는 공연도 해야하고 동시에 여자 주인공도 공연에 직접 초빙해야 하는 다양한 책임을 가지고 있다.
AppConfig
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());
}
}
MemberSerciceImpl - 생성자 주입
package hello.core.member;
import hello.core.order.OrderServiceImpl;
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);
}
}
MemoryMemberRepository
(구현체)를 의존하지 않고, MemberRepository
(인터페이스)에만 의존한다.MemberSerciceImpl
입장에서 생성자를 통해 어떤 구현체가 들어올지(주입될지) 알 수 없으며, 생성자를 통해 어떤 구현 객체를 주입할지는 외부(AppConfig)에서 결정한다.MemberServiceImpl
은 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다. 클래스 다이어그램
회원 객체 인스턴스 다이어그램
OrderServiceImpl - 생성자 주입
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
은 이제 실행에만 집중. 생성자를 통해 외부(AppConfing)에서 어떤 구현 객체를 주입할지 결정한다.
사용클래스 - MemberApp
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("findMember : " + findMember.getName());
}
}
appConfig로부터 MemberService를 받아서 적용.
사용클래스 - 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;
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);
}
}
테스트 코드 수정
MemberServiceImplTest
class MemberServiceImplTest {
MemberService memberService;
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
}
}
OrderServiceImplTest
public class OrderServiceTest {
MemberService memberService;
OrderService orderService;
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
}
}
정리
- AppConfig가 공연 기획자 역할을 수행하면서 관심사를 확실히 분리했다.
- AppConfig는 구체 클래스를 선택한다. 배역에 맞는 담당 배우를 선택한다. 애플리케이션이 어떻게 동작해야 할지 전체 구성을 책임진다.
- 이제 각각의 배우(OrderServiceImpl, MemberServiceImpl)들은 기능을 실행하는 책임만 지면 된다.
현재 AppConfig의 문제점:
기대하는 그림
어떤 인터페이스들이 있고, 해당 인터페이스들에 대해 어떤 구현체를 사용하는지 한눈에 파악 가능해야 함
ctrl + alt + M: 메서드 추출 기능
return type: interface (구체 클래스로 선택 하지 않는다)
name: memberRepository()
중복값 알림창(intellij): replace 선택해 중복값 모두 바꾸도록.
리팩터링 후
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 MemoryMemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
메서드명 만으로도 역할이 다 드러남.
각각의 인터페이스(OrderService, discountPolicy ...)에 어떤 구현체를 사용하는지 한눈에 확인 가능.
설계에 대한 그림이 구성정보에 그대로 드러남. 역할들이 나오고, 역할들의 구현을 선명히 표기 가능.
- 그냥 코드를 작성하는게 아니라, 역할을 분명히 세우고 그 안에 구현이 들어가도록 코드를 작성했음.
- 중복제거 및 직관성 획득
이제 정액할인 정책을 “정률 할인 정책”으로 변경해보자.
어떤 부분만 변경하면 될까?
AppConfig의 등장으로 애플리케이션이 크게 사용영역과, 객체를 생성하고 구성(Configuration)하는 영역으로 분리 되었다.
할인 정책을 변경하는 경우
코드 변경 시 구성 영역만 영향을 받고, 사용 영역은 전혀 영향을 받지 않는다.
할인 정책 변경 코드
public class AppConfg {
public DiscountPolicy discountPolicy() {
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
구성 영역은 당연히 변경된다. 구성 역할을 담당하는 AppConfig를 애플리케이션이라는 공연의 기획자로 생각하자. 공연 기획자는 공연 참여자인 구현 객체들을 모두 알아야 한다.
새로운 할인 정책 개발
: 다형성(인터페이스 - 클래스)으로 추가 코드 개발은 쉬웠음.
새로운 할인 정책 적용과 문제점
DIP위반(DiscountPolicy discountPolicy = new FixDiscountPolicy): 인터페이스와 클래스 동시 의존
=> 할인을 변경하니 클라이언트 코드도 함께 변경해야 함.
관심사의 분리
AppConfig의 등장: 구현 객체 생성 & 연결을 책임.
클라이언트 객체는 자신의 역할 실행에만 집중 + 권한 축소 (=책임이 명확해짐)
AppConig 리팩터링
"역할vs구현"이 명확히 표기되도록 코드 변경
“메서드명(인터페이스) : 반환값(클래스)” 형태. 직관적으로 인터페이스와 구현체의 관계를 확인가능
새로운 구조와 할인 정책 적용
AppConfig의 등장으로 애플리케이션이 크게 사용 영역과, 객체를 생성하고 구성하는 영역으로 분리
구현체가 변경되어도, 구성 영역만 변경하면 됨. 사용 영역은 변경할 필요가 없음.(클라이언트 코드 변경 필요하지 않음)
3가지가 적용됨: SRP, DIP, OCP
한 클래스는 하나의 책임만 가져야 한다.
프로그래머는 “추상화에 의존해야지 구체화에 의존하면 안된다.” 의존성 주입은 이 원칙을 따르는 장법 중 하나다.
DiscountPolicy discountPolicy = new FixDiscountPolicy();
DiscountPolicy discountPolicy;
public OrderServiceImpl(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
OrderServiceImpl
)가 직접 객체를 생성, 연결, 실행하며 프로그램의 ‘제어 흐름(Control)’을 스스로 조종했다. 개발자 입장에서는 자연스러운 흐름이다.프레임워트 vs 라이브러리
- 프레임워크: 내가 작성한 코드를 대신 제어하고 실행 (JUnit)
- 라이브러리: 내가 작성한 코드가 직접 제어의 흐름을 담당
OrderServiceImpl
은 DiscountPolicy
인터페이스에 의존한다. 실제 어떤 구현 객체가 사용될지는 모른다.클래스가 사용하는 import 코드만 보고 의존관계를 쉽게 판단할 수 있다. 정적인 의존관계는 애플리케이션을 실행하기도 전에, 클래스 다이어그램만으로 의존관계가 파악 가능하다.
그런데 이러한 클래스 의존관계만으로는 실제 어떤 객체가 OrderServiceImpl
에 주입될지 알 수 없다.
(클래스 다이어그램)
애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계다.
(객체 다이어그램)
IoC 컨테이너, DI 컨테이너
- AppConfig: IoC컨테이너, DI 컨테이너, 어셈블러(조합해준다고해서), 오브젝트 팩토리 etc..
- IoC의 의미가 광범위해 DI로 축소. 주로 DI컨테이너라 부름
지금까지 순수 자바 코드로 DI를 적용했다. 이제 스프링을 사용해보자!!
AppConfig 스프링 기반으로 변경
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
: AppConfig에 설정을 구성한다는 뜻
-각 메서드 위에 @Bean
: 스프링 컨테이너에 스프링 빈으로 등록함
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("findMember : " + findMember.getName());
}
}
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);
}
}
@Bean으로 등록해 놓은 것들이 스프링 컨테이너에 메서드 이름으로 등록됨.
이 이름으로 컨테이너에서 찾고, 해당 타입으로 돌려받음.
실제 스프링 코드에서 어떤 이름으로 빈들이 등록됐는지 확인 가능.
@Configuration
이 붙은 AppConfig
를 설정(구성) 정보로 사용함. 여기서 @Bean
이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록함. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다.@Bean
이 붙은 메서드 명을 스프링 빈의 이름으로 사용한다.(memberService
, OrderService
)applicationContext.getBean()
메서드를 사용해서 찾을 수 있다.코드가 더 복잡해진 것 같은데, 스프링 컨테이너를 사용하면 어떤 장점이 있을까??
요약하자면, 많은 장점을 가져갈 수 있다. 앞으로 알아보자!!