[Spring] 스프링 핵심 원리 - 기본편 섹션 3 스프링 핵심 원리 이해2 - 객체 지향 원리 적용
[Spring] 스프링 핵심 원리 - 기본편 섹션 3 스프링 핵심 원리 이해1 - 새로운 할인 정책 개발
📄 새로운 할인 정책 개발
- 정률 % 할인 적용
- 👤 기획자: 정률 % 할인으로 바꿔야해요. 예를들어, 10%로 지정해두면 고객이 10000원 주문시 1000원을 할인해주고, 20000원 주문시에 2000원을 할인하게 바꿔주세요.
우리는 앞서 이 사태를 예상해 설계했으므로... 정률 할인 정책클래스를 만든다.
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;
}
}
}
* 정률 % 할인 테스트
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가 아니면 할인이 적용되지 않아야 한다.")
void vip_x() {
//given
Member member = new Member(2L, "memberBASIC", Grade.BASIC);
//when
int discount = discountPolicy.discount(member, 10000);
//then
assertThat(discount).isEqualTo(0);
}
}
🖥️ 새로운 할인 정책 적용과 문제점
할인 정책을 변경하려면 할인 정책 클라이언트인 OrderServiceImpl 코드를 고쳐야 한다.
public class OrderServiceImpl implements OrderService{
//private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final RateDiscountPolicy discountPolicy = new RateDiscountPolicy();
역할역할과 구현을 충실하게 분리하고, 다형성과 인터페이스를 활용하여 구현 객체를 분리했다.
하지만 SOLID 원칙 DIP,OCP는 지켜지지않았다.
- 추상(인터페이스) DiscountPolicy는 구체(구현) 클래스 FixDiscountPolicy , RateDiscountPolicy에 의존한다.
- 변경 없이 기능을 확장할 수 있다고 했지만, 현재 코드는 기능을 확장하면 클라이언트 코드에 영향을 준다. 위를 보시라. OrderServiceImpl 코드를 변경해야 한다. 따라서 OCP를 위반한다.
그래서 생각한 것이...인터페이스만 코드에 남겨두는 것이다. 아래처럼 변경!
private DiscountPolicy discountPolicy;
실제 실행을 해보면 구현체가 없기에 NPE(null pointer exception)가 발생한다.
🧐...결국 이 문제를 해결하려면 제3자가 클라이언트인 OrderServiceImpl 에 DiscountPolicy 의 구현 객체를 대신 생성하고 주입해주어야 한다!
하하..즉, 공연으로 치면 로미오 배우가 줄리엣 배우를 고르고 있는 꼴...그러므로 관심사를 분리하고 공연 기획자(제3자)를 만들 필요가 있다!
🟢AppConfig 등장
Application 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스를 만든다.
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(
new MemoryMemberRepository(),
new FixDiscountPolicy());
}
}
그리고 memberServiceImpl에서 생성자 주입을 해준다.그럼 이제 아래 코드를 보면 알 수 있는 해당 클래스는 추상화에만 의존한다.
- AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
- AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입(연결)해준다.
MemberServiceImpl 은 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다.
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
appConfig 객체는 memoryMemberRepository 객체를 생성하고 그 참조값을 memberServiceImpl 을 생성하면서 생성자로 전달한다. 클라이언트인 memberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고 해서 DI(Dependency Injection) 우리말로 의존관계 주입 또는 의존성 주입이라 한다.
이제 MemberApp, Order App에 AppConfig를 생성하고, 이를 쓰도록한다.
public class MemberApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService(); // appconfig에서 모두 결정
//MemberService memberService = new MemberServiceImpl(memberRepository);
Member memberA = new Member(1L, "memberA", Grade.VIP);
memberService.join(memberA);
Member findMember = memberService.findMember(1L);
System.out.println("new Member = " + memberA.getName());
System.out.println("findMember = " + findMember.getName());
}
}
public class OrderApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService(); // appconfig에서 모두 결정
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());
}
}
테스트 코드 또한 아래처럼 변경해 준다. 테스트 코드에서 @BeforeEach 는 각 테스트를 실행하기 전에 호출된다.
public class MemberServiceTest {
//MemberService memberService = new MemberServiceImpl(memberRepository);
MemberService memberService;
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
}
public class OrderServiceTest {
//MemberService memberService = new MemberServiceImpl(memberRepository);
//OrderService orderService = new OrderServiceImpl(memberRepository, discountPolicy);
MemberService memberService;
OrderService orderService;
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
}
🪄 AppConfig 리팩터링
public class AppConfig {
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
}
각 객체 생성(new) 부분을 별도 분리해주었다. 이제 AppConfig 를 보면 역할과 구현 클래스가 한눈에 들어온다. 애플리케이션 전체 구성을 빠르게 파악할 수 있다.
🪄 새로운 구조와 할인 정책 적용
이제 정액 할인 정책을 정률% 할인 정책으로 변경하려면, FixDiscountPolicy를 RateDiscountPolicy로만 바꾸면 된다.
사용 영역은 전혀 영향을 받지 않는다. 구성 영역은 당연히 변경된다.
public DiscountPolicy discountPolicy() {
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
🖊️ 좋은 객체 지향 설계의 5가지 원칙의 적용
여기서는 3가지 SRP, DIP, OCP 적용된 것을 확인 가능하다.
▪️SRP 단일 책임 원칙
한 클래스는 하나의 책임만 가져야 한다.
구현 객체를 생성하고 연결하는 책임은 AppConfig가 담당 클라이언트 객체는 실행하는 책임만 담당한다.
▪️ DIP 의존관계 역전 원칙
프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다.
클라이언트 코드가 DiscountPolicy 추상화 인터페이스에만 의존하고 구체화 구현 클래스에 의존하지 않는다.
▪️ OCP
소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
AppConfig가 의존관계를 FixDiscountPolicy→RateDiscountPolicy 로 변경해서 주입하므로 클라이언트 코드는 변경 X
소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀 있다!
⭐ IoC, DI, 그리고 컨테이너
제어의 역전(IoC) (Inversion of Control)
프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것
의존관계 주입 DI(Dependency Injection)
의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.
- 정적인 클래스 의존관계 : 클래스가 사용하는 import 코드만 보고 의존관계를 쉽게 판단할 수 있다.
- 동적인 객체 인스턴스 의존 관계 : 애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계다.
IoC 컨테이너, DI 컨테이너
AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것. (어샘블러, 오브젝트 팩토리 등으로 불림)
🎩 스프링으로 전환하기
@Configuration
public class AppConfig {
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
AppConfig에서 @Configuration과 @Bean 설정
public class MemberApp {
public static void main(String[] args) {
// AppConfig appConfig = new AppConfig();
// MemberService memberService = appConfig.memberService();
// MemberService memberService = new MemberServiceImpl(memberRepository);
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
위처럼 ApplicationContext를 이용한 코드로 바꿔준다.
🟢 스프링 컨테이너
- ApplicationContext 를 스프링 컨테이너라 한다.
- 기존에는 개발자가 AppConfig 를 사용해서 직접 객체를 생성하고 DI를 했지만, 이제부터는 스프링 컨테이너를 통해서 사용한다. 스프링 컨테이너는 @Configuration 이 붙은 AppConfig 를 설정(구성) 정보로 사용한다.
- 여기서 @Bean 이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다.
- 스프링 빈은 @Bean 이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다. ( memberService ,
orderService ) - 스프링 빈은 applicationContext.getBean() 메서드를 사용해서 찾을 수 있다.