스프링 핵심 원리 이해2 - 객체 지향 원리 적용
새로운 할인 정책 개발
새로운 할인 정책을 확장
악덕 기획자: 서비스 오픈 직전에 할인 정책을 지금처럼 고정 금액 할인이 아니라 좀 더 합리적인 주문 금액당 할인하는 정률% 할인으로 변경하고 싶어요. 예를 들어서 기존 책은 VIP가 10000원을 주문하든 20000원을 주문하든 항상 1000원을 할인했는데, 이번에 새로 나온 정책은 10%로 지정해두면 고객이 10000원 주문시 1000원을 할인해주고, 20000원 주문시에 2000원을 할인해주는 거에요!
순진 개발자: 제가 처음부터 고정 금액 할인은 아니라고 했잖아요.
악덕 기획자: 애자일 소프트웨어 개발 선언 몰라요? “계획을 따르기보다 변화에 대응하기를”
순진 개발자: … (하지만 난 유연한 설계가 가능하도록 객체지향 설계 원칙을 준수했지 후후)
참고: 애자일 소프트웨어 개발 선언 https://agilemanifesto.org/iso/ko/manifesto.html
순진 개발자가 정말 객체지향 설계 원칙을 잘 준수 했는지 확인해보자. 이번에는 주문한 금액의 %를 할인해주는 새로운 정률 할인 정책을 추가하자.
RateDiscountPolicy 추가
RateDiscountPolicy 코드
// implements로 DiscountPolicy 기능을 불러온 후 재정의
// Grade.VIP에 해당하는 회원들에게 10%할인부여
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;
}
}
}
RateDiscountPolicyTest 코드 추가
// 할인이 적용되는 테스트 검증
// @DisplayName으로 해당 기능들이 적용되는 지 확인
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(2L,"memberBASIC", Grade.BASIC);
//when
int discount = discountPolicy.discount(member, 10000);
//then
assertThat(discount).isEqualTo(0);
}
}
새로운 할인 정책 적용과 문제점
방금 추가한 할인 정책을 적용
할인 정책을 애플리케이션에 적용해보자
// 의존관계에서 DIP, OCP 위반
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 int discount(Member member, int price) {
return 0;
}
};
Problem
클래스 다이어그램으로 의존관계 분석
기대했던 의존관계
실제 의존관계
정책 변경
문제 해결
인터페이스에만 의존하도록 설계를 변경하자
인터페이스에만 의존하도록 코드 변경
// 구현체가 없는데 코드 실행은 가능한가?
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository = new MemoryMemberRepository();
private DiscountPolicy discountPolicy;
...
}
// 실행하면 java.lang.NullPointerException 오류가 뜸
관심사의 분리
AppConfig
코드추가
// AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성
// 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());
}
}
참고: 지금은 각 클래스에 생성자가 없어서 컴파일 오류가 발생한다. 바로 다음에 코드에서 생성자를 만든다.
MemberServiceImpl - 생성자 주입
// 설계 변경으로 MemberServiceImpl은 MemoryRepository를 의존하지 않음 -> MemberRepository 인터페이스만 의존
// MemberServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 주입될지 알 순 없음
// MemberServiceImpl의 생성자를 통해서 어떤 구현 객체가 주일할지는 오직 외부(AppConfig)에서 결정
// MemberServiceImpl은 의존관계에 대한 고민은 외부에 맡긴다 -> 실행에만 집중
package hello.core.member;
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
...
}
클래스 다이어그램
회원 객체 인스턴스 다이어그램
OrderServiceImpl - 생성자 주입
// 설계 변경으로 orderServiceImpl은 FixDiscountPolicy을 의존하지 않음 -> DiscountPolicy 인터페이스만 의존
// OrderServiceImpl입장에서 생성자를 통해 어떤 구현 객체가 주입될 지 알 수 없음
// OrderServiceImpl에는 MemoryMemberRepository, FixDiscountPolicy 객체의 의존관계가 주입
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;
private DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository,DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
...
}
AppConfig 실행
사용 클래스 - MemberApp
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();
...
}
}
사용 클래스 - OrderApp
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();
...
}
}
테스트 코드 오류 수정
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();
}
...
}
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();
...
}
}
AppConfig 리팩터링
이상적인 다이어그램
/////////////////////////////////////////리펙터링 전
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService(){
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
/////////////////////////////////////////리펙터링 후
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(MemberRepository());
}
private MemberRepository MemberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService(){
return new OrderServiceImpl(MemberRepository(), discountPolicy());
}
public DiscountPolicy discountPolicy(){
return new FixDiscountPolicy();
}
}
// new MemoryMemberRepository() 이 부분이 중복 제거되었다.
// 이제 MemoryMemberRepository 를 다른 구현체로 변경할 때 한 부분만 변경하면 된다.
// AppConfig 를 보면 역할과 구현 클래스가 한눈에 들어온다.
// 애플리케이션 전체 구성이 어떻게 되어있는지 빠르게 파악할 수 있다.
새로운 구조와 할인 정책 적용
사용, 구성의 분리
할인 정책의 변경
할인 정책 변경 구성 코드
//AppConfig에서 할인 정책 역할을 담당하는 구현을 FixDiscountPolicy -> RateDiscountPolicy로 변경
// 이제 할인 정책을 변경해도, 애플리케이션의 구성 역할을 담당하는 AppConfig만 변경
// 클라이언트 코드인 OrderServiceImpl를 포함해서 사용 영역의 어떤 코드도 변경할 필요없음
// 구성 영역은 당연히 변경된다. 구성 역할을 담당하는 AppConfig를 애플리케이션이라는 공연의 기획자로 생각하자. 공연 기획자는 공연 참여자인 구현 객체들을 모두 알아야 한다.
...
public DiscountPolicy discountPolicy(){
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
...
총괄 정리
SRP 단일 책임 원칙
: 한 클래스는 하나의 책임만 가져야 한다.
DIP 의존관계 역전 원칙
OCP
소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있음
제어의 역전 IoC(Inversion of Control)
프레임워크 vs 라이브러리
의존관계 주입 DI(Dependency Injection)
정적인 클래스 의존관계
클래스 다이어그램
동적인 객체 인스턴스 의존 관계
객체 다이어그램
IoC 컨테이너, DI 컨테이너
AppConfig 스프링 기반으로 변경
...
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();
}
}
MemberApp에 스프링 컨테이너 적용
...
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);
...
OrderApp에 스프링 컨테이너 적용
...
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);
...
스프링 컨테이너