[스프링 핵심원리] 2. 객체지향 설계와 스프링, 리팩터링
스프링 핵심 원리 - 기본편 - 인프런 | 강의
스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런
www.inflearn.com
다음 강의를 완강하고, 복습 차 정리를 시작합니다
먼저 이전 코드에서 클라이언트 코드가 구현체를 참조하고 있기 때문에, 코드에 리팩터링이 필요하다는 짧은 코멘트를 남겼습니다
다시 수업 내용을 따라가보면서, 왜? 구현체를 의존하는 것이 좋지 않은지 코드가 어떻게 구성되어 있던건지 자세히 알아보겠습니다.
새로운 할인 정책이 등장했다고 가정합시다
아까 우리는 할인 정책은 확정된 것이 아니지만, 일단 고정 금액 할인 정책을 적용하기로 마음먹고
FixDiscountPolicy 구현체를 생성했습니다(DiscountPolicy 인터페이스의 구현체)
하지만, 이제 퍼센티지 할인 정책을 기획자가 요구하기 시작했습니다
그러면 RateDiscountPolicy 구현체를 추가해야합니다.
package hello.springintroduction.discount;
import hello.springintroduction.Member.Grade;
import hello.springintroduction.Member.Member;
import org.springframework.stereotype.Component;
@Component
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;
}
}
}
기획자의 요구에 따라 다음과 같이 RateDiscountPolicy 코드를 추가하였습니다.
이 코드를 테스트하기 위한 Junit 테스트 코드를 작성하겠습니다.
package hello.springintroduction.discount;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
import hello.springintroduction.Member.Grade;
import hello.springintroduction.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 discount() {
// given
Member memberVIP = new Member(1L, "memberVIP", Grade.VIP);
// when
int discount = discountPolicy.discount(memberVIP, 10000);
// then
assertThat(discount).isEqualTo(1000);
}
@Test
@DisplayName("VIP가 아닌 경우 할인이 적용되어서는 안된다.")
void vip_x(){
// given
Member memberVIP = new Member(1L, "memberVIP", Grade.BASIC);
// when
int discount = discountPolicy.discount(memberVIP, 10000);
// then
assertThat(discount).isNotEqualTo(1000);
//option + enter를 통해 static import 권장
}
}
이전에 서술했던대로 Given/ When/ Then 구조를 차용하였습니다.
그에 더해, 예외상황에 제대로 예외가 처리되는지도 테스트 하기 위해 If Not 에 해당하는 vip_x() 테스트 메소드를 추가 작성하였습니다.
강의 중 노트로, 테스트 코드의 경우 조건을 만족하는 테스트와 만족하지 않는 경우 제대로 코드가 작동하는지(제대로 분기하는지)
모두 코드로 테스트 해야 할 필요가 있다고 합니다.
이제 문제는 방금 추가한 할인정책을 클라이언트 코드에 직접 적용할 때 발생합니다.
할인 정책을 변경하려면 우리가 이전에 작성했던 OrderServiceImpl의 코드를 변경해야 합니다.
package hello.springintroduction.order;
import hello.springintroduction.Member.Member;
import hello.springintroduction.Member.MemberRepository;
import hello.springintroduction.Member.MemoryMemberRepository;
import hello.springintroduction.discount.DiscountPolicy;
import hello.springintroduction.discount.FixDiscountPolicy;
import hello.springintroduction.discount.RateDiscountPolicy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class OrderServiceImpl implements OrderService {
// 이 부분 의존성 관련 코드의 수정이 필요하게 됩니다
private MemberRepository memberRepository = new MemoryMemberRepository();
private DiscountPolicy discountPolicy = new FixDiscountPolicy;
// DiscountPolicy만 의존하는게 아니라 FixDiscountPolicy도 의존하고 있었다!!
// DIP 위반임. 결론적으로 정책 변경으로 인해, OrderServiceImpl의 코드 또한 변경해야함.
// DiscountPolicy 확장해서 변경하면 클라이언트 코드에 영향을 주므로 OCP 위반
@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);
}
}
주석에서 서술한 대로, DiscountPolicy 추상 객체만 의존하는게 아니라 구현 객체도 의존하고 있다는 점에서
정책 변경으로 인해 OrderServiceImpl의 코드 또한 변경해야하고, DiscountPolicy를 확장해서 변경하면 OrderServiceImpl 즉 클라이언트 코드에 영향을 줄 수 있기 때문에 OCP 위반에 해당합니다.
역할과 구현을 충실하게 분리했고 (인터페이스, 구현체 분리)
다형성을 활용하였지만 OCP와 DIP를 위반한게 됩니다.
Open-Closed Principle
- 기능을 확장/변경하면 클라이언트 코드에 영향을 줌
Dependency Inversion Principle
- OrderServiceImpl은 구체 클래스에 의존하고 있음
사실 우리는 이렇게 코드를 작성하고 있었던 겁니다.
그러면 어떻게 해결해야 할까요?
당연히 인터페이스에만 의존하도록 코드를 변경해야겠습니다.
하지만 구현체가 없으면 코드가 다음과 같이 변경될텐데 NullPointerException이 발생하게 될겁니다.
package hello.springintroduction.order;
import hello.springintroduction.Member.Member;
import hello.springintroduction.Member.MemberRepository;
import hello.springintroduction.Member.MemoryMemberRepository;
import hello.springintroduction.discount.DiscountPolicy;
import hello.springintroduction.discount.FixDiscountPolicy;
import hello.springintroduction.discount.RateDiscountPolicy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
//private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private DiscountPolicy discountPolicy; // Interface에만 의존하게 됨.
// 이 상태로만 두면 NPE -> 구체 클래스 어떻게 주입?
// DiscountPolicy만 의존하는게 아니라 FixDiscountPolicy도 의존하고 있었다!!
// DIP 위반임. 결론적으로 정책 변경으로 인해, OrderServiceImpl의 코드 또한 변경해야함.
// DiscountPolicy 확장해서 변경하면 클라이언트 코드에 영향을 주므로 OCP 위반
@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에 DiscountPolicy의 구현체를 주입해주어야 합니다!
따라서 우리는 관심사를 분리할 필요가 있습니다.
클라이언트 코드는, 클라이언트에만 집중하고 이 클라이언트 코드들에 주입해줄 다른 친구를 도입할겁니다
이름하야 AppConfig를 작성해보겠습니다.
package hello;
import hello.springintroduction.Member.MemberRepository;
import hello.springintroduction.Member.MemberService;
import hello.springintroduction.Member.MemberServiceImpl;
import hello.springintroduction.Member.MemoryMemberRepository;
import hello.springintroduction.discount.DiscountPolicy;
import hello.springintroduction.discount.FixDiscountPolicy;
import hello.springintroduction.discount.RateDiscountPolicy;
import hello.springintroduction.order.OrderService;
import hello.springintroduction.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// 사용영역과 구성 영역으로 구역이 이분할 됨.
// 사용영억(클라이언트 코드), 구성영역 (DIP 코드)
@Configuration
public class AppConfig { // 생성자를 통한 주입
// 각각의 Impl에 생성자를 구현함으로써 Dependency injection이 가능함.
// XML로 사용하면 AppConfig.java 조차 필요가 없다!
@Bean
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
}
@Bean
public OrderService orderService(){
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
AppConfig는 애플리케이션에 실제 필요한 구현객체를 생성해줍니다.
생성한 인스턴스의 참조를 생성자를 통해서 주입, 연결해줍니다.
설계 변경으로 MemberServiceImpl은 MemoryMemberRepository를 의존하지 않습니다.
이제 단지 MemberRepository 인터페이스만 의존하게 될 뿐입니다.
appConfig 객체는 MemoryMemberRepository 객체를 생성하고 그 참조값을 memberServiceImpl을 생성하면서 생성자로 전달
클라이언트 입장에서 보면 의존관계를 외부에서 주입해주기 때문에 DI(Dependency Injection) 이라고 한다.
package hello.springintroduction.order;
import hello.springintroduction.Member.Member;
import hello.springintroduction.Member.MemberRepository;
import hello.springintroduction.Member.MemoryMemberRepository;
import hello.springintroduction.discount.DiscountPolicy;
import hello.springintroduction.discount.FixDiscountPolicy;
import hello.springintroduction.discount.RateDiscountPolicy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
//private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private DiscountPolicy discountPolicy; // Interface에만 의존하게 됨.
// 이 상태로만 두면 NPE -> 구체 클래스 어떻게 주입?
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy){
this.memberRepository=memberRepository;
this.discountPolicy=discountPolicy;
}
// DiscountPolicy만 의존하는게 아니라 FixDiscountPolicy도 의존하고 있었다!!
// DIP 위반임. 결론적으로 정책 변경으로 인해, OrderServiceImpl의 코드 또한 변경해야함.
// DiscountPolicy 확장해서 변경하면 클라이언트 코드에 영향을 주므로 OCP 위반
@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);
}
}
이제 AppConfig가 OrderServiceImpl의 생성자를 통해 객체를 생성할때 생성자로 전달한다
MemoryMemberRepository, FixDiscountPolicy 객체의 의존관계가 주입된다.
AppConfig 실행
package hello.springintroduction;
import hello.AppConfig;
import hello.springintroduction.Member.Grade;
import hello.springintroduction.Member.Member;
import hello.springintroduction.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("findMember = " + findMember);
System.out.println("member = " + member);
}
}
package hello.springintroduction;
import hello.AppConfig;
import hello.springintroduction.Member.Grade;
import hello.springintroduction.Member.Member;
import hello.springintroduction.Member.MemberService;
import hello.springintroduction.Member.MemberServiceImpl;
import hello.springintroduction.order.Order;
import hello.springintroduction.order.OrderService;
import hello.springintroduction.order.OrderServiceImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class OrderApp {
public static void main(String[] args) {
OrderService orderService = new OrderServiceImpl();
MemberService memberService = new MemberServiceImpl();
//ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
//MemberService memberService = ac.getBean("memberService", MemberService.class);
//OrderService orderService = ac.getBean("orderService", OrderService.class);
Long memberId=1L;
Member member= new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 1000);
System.out.println("order = " + order);
}
}
다음과 같은 방법으로 우리가 작성한 AppConfig의 실행이 가능하다.
package hello.springintroduction.member;
import hello.AppConfig;
import hello.springintroduction.Member.Grade;
import hello.springintroduction.Member.Member;
import hello.springintroduction.Member.MemberService;
import hello.springintroduction.Member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
public class MemberServiceTest {
AppConfig appConfig = new AppConfig();
MemberService 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(findMember).isEqualTo(member);
}
}
package hello.springintroduction.order;
import hello.AppConfig;
import hello.springintroduction.Member.Grade;
import hello.springintroduction.Member.Member;
import hello.springintroduction.Member.MemberService;
import hello.springintroduction.Member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
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 = new Order(memberId, "itemA", 10000, 1000);
Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
}
}
@BeforeEach의 경우, 각 테스트를 실행하기 전에 실행된다는 사실을 기억하자.
추가로 AppConfig의 가독성 향상을 위해 다음과 같이 코드를 개선했습니다.
package hello;
import hello.springintroduction.Member.MemberRepository;
import hello.springintroduction.Member.MemberService;
import hello.springintroduction.Member.MemberServiceImpl;
import hello.springintroduction.Member.MemoryMemberRepository;
import hello.springintroduction.discount.DiscountPolicy;
import hello.springintroduction.discount.FixDiscountPolicy;
import hello.springintroduction.discount.RateDiscountPolicy;
import hello.springintroduction.order.OrderService;
import hello.springintroduction.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// 사용영역과 구성 영역으로 구역이 이분할 됨.
// 사용영억(클라이언트 코드), 구성영역 (DIP 코드)
@Configuration
public class AppConfig { // 생성자를 통한 주입
// 각각의 Impl에 생성자를 구현함으로써 Dependency injection이 가능함.
// XML로 사용하면 AppConfig.java 조차 필요가 없다!
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
/*public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}*/
@Bean
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
역할에 따른 구현을 고려하여 코드를 수정했습니다.
이제는 구현체를 변경할 경우, 더 이상 클라이언트 코드를 건드리지 않고 AppConfig를 통해 한줄만 수정해주면 모든게 해결됩니다!
다음 시간에는 전체 흐름을 정리하면서, DI, IOC, 컨테이너에 대해 정리하겠습니다.