1. 새로운 할인 정책 개발
할인 정책 인터페이스를 구현하는 새로운 구현 클래스인 정률 할인 정책 클래스를 생성한다.
src/main/java/discount에 RateDiscountPolicy 클래스를 생성한다.
import hello.core1.member.Grade;
import hello.core1.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;
}
}
}
src/test/java/discount에 RateDiscountPolicyTest 클래스를 생성해 위 코드를 테스트한다.
import hello.core1.member.Grade;
import hello.core1.member.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.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
Assertions.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
Assertions.assertThat(discount).isEqualTo(1000); //원칙상 0이어야 하므로 실패
}
}
cf) Test 클래스 생성 : Ctrl+Shift+T 키 (Windows)
2. 새로운 할인 정책 적용과 문제점
RateDiscountPolicy를 적용해서 할인 정책을 바꾸려면 클라이언트인 OrderServiceImpl 을 수정해야 한다.
public class OrderServiceImpl implements OrderService {
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
-문제점
- 우리는 역할과 구현을 충실하게 분리했다. -> OK
- 다형성도 활용하고, 인터페이스와 구현 객체를 분리했다. -> OK
- OCP, DIP 같은 객체지향 설계 원칙을 충실히 준수했다. -> 그렇게 보이지만 사실은 아니다.
- DIP 위반 -> 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있다. 추상(인터페이스) 의존: DiscountPolicy 구체(구현) 클래스: FixDiscountPolicy , RateDiscountPolicy
- OCP 위반 -> 지금 코드는 기능을 확장해서 변경하면, 클라이언트 코드에 영향을 준다.
-의존관계
DiscountPolicy 인터페이스에만 의존한다고 생각했었다.
그러나 실제로는 DiscountPolicy 인터페이스와 FixDiscountPolicy 구체 인터페이스를 모두 의존한다.
-> DIP 위반
FixDiscountPolicy를 RateDiscountPolicy로 할인 정책을 변경하게 되면 OrderServiceImpl의 소스코드도 변경해야 한다
-> OCP 위반
-해결 방안
DIP를 위반하지 않고 인터페이스에만 의존하도록 의존관계를 변경하면 된다.
수정한 코드는 아래와 같다.
public class OrderServiceImpl implements OrderService {
//private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private DiscountPolicy discountPolicy;
}
그러나 구현체가 없기 때문에 실제 실행을 하면 NPE(NULL Pointer Exception)가 발생한다.
이를 해결하기 위해서는 클라이언트인 OrderServiceImpl에 DiscountPolicy의 구현 객체를 대신 생성하고 주입해야 한다.
3. 관심사의 분리
OrderService는 DiscountPolicy를 어떤 구현체로 쓸지 정할 수 없다.
MemberService도 어떤 MemberRepository를 구현체로 쓸지 정할 수 없다.
구현객체를 생성하고 연결하는 것은 공연기획자 역할이 필요하다. 그 역할을 하는 클래스를 생성해보자.
src/main/java에 AppConfig 클래스를 생성한다.
import hello.core1.member.MemberService;
import hello.core1.member.MemberServiceImpl;
import hello.core1.member.MemoryMemberRepository;
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
}
위 코드는 구현 객체인 MemoryMemberRepository를 생성하고 연결해준다.
다음으로 생성자를 주입하기 위해 MemberServiceImpl을 수정해야 한다.
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository;
//생성자 주입
public MemberServiceImpl(MemberRepository memberRepository){
this.memberRepository=memberRepository;
}
MemoryMemberRepository에 의존하지 않고, MemberRepository에만 의존하도록 한다.
이때 MemberServiceImpl은 어떤 구현 객체가 주입될지 알 수 없다.
MemberServiceImpl의 생성자를 통해서 어떤 구현 객체를 주입할지는 외부인 AppConfig에서 결정된다.
AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해 주입(연결)한다.
OrderService도 위와 같은 원리로 코드를 수정하면 된다.
우선 AppConfig에 아래 코드를 추가한다.
public OrderService orderService() {
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
OrderServiceImpl 클래스에 아래 코드를 추가해 수정한다.
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
} // 생성자 주입
객체의 생성과 연결은 appConfig가 담당한다.
- DIP 완성 : MemberServiceImpl은 MemberRepository인 추상에만 의존하면 된다. 이제 구체 클래스를 몰라도 된다.
- 관심사의 분리 : 객체를 생성하고 연결하는 역할과 실행하는 역할이 명확히 분리되었다.
appConfig는 memoryMemberRepository를 생성하고, 그 참조값을 memberServiceImpl을 생성하면서 생성자로 전달한다.
-AppConfig 실행
MemberApp에 AppConfig를 이용해 아래 코드를 추가하여 수정 가능하다.
public class MemberApp{
AppConfig appConfig=new AppConfig();
MemberService memberService=appConfig.memberService();
// MemberService memberService=new MemberServiceImpl();
}
아래 코드를 참고한 것이다.
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
}
OrderApp도 동일하게 수정 가능하다. 아래 코드를 추가한다.
public class OrderApp {
public static void main(String[] args) {
AppConfig appConfig=new AppConfig();
MemberService memberService=appConfig.memberService();
OrderService orderService= appConfig.orderService();
//MemberService memberService=new MemberServiceImpl();
//OrderService orderService=new OrderServiceImpl();
}
아래 코드를 참고한 것이다.
public class AppConfig {
public OrderService orderService() {
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
-Test 코드 수정
AppConfig를 이용하여 Test 클래스를 수정해야 한다.
먼저 MemberServiceTest 클래스를 아래 코드를 추가하여 수정한다.
class MemberServiceTest {
MemberService memberService;
@BeforeEach // test 실행 전 무조건 실행하도록 함
public void beforeEach(){
AppConfig appConfig=new AppConfig();
memberService=appConfig.memberService();
}
//MemberService memberService = new MemberServiceImpl();
OrderServiceTest도 동일하게 아래 코드를 추가하여 수정한다.
class OrderServiceTest {
MemberService memberService;
OrderService orderService;
@BeforeEach
public void beforeEach(){
AppConfig appConfig=new AppConfig();
memberService =appConfig.memberService();
orderService= appConfig.orderService();
}
4. AppConfig 리팩터링
AppConfig의 중복을 제거하고 역할에 따른 구현을 한눈에 볼 수 있게 수정한다.
이를 통해 애플리케이션 전체 구성이 어떻게 되어있는지 빠르게 파악할 수 있다.
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
위 코드에서 MemoryMemberRepository가 중복되어 있다.
-리팩터링 후
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 FixDiscountPolicy();
}
}
cf) 중복 제거 리팩토링 : Ctrl+Alt+M 키
5. 새로운 구조와 할인 정책 적용
정책 할인 정책을 정률 할인 정책으로 변경하고자 한다.
이를 위해서 구성 영역인 AppConfig 부분만 변경하면 된다.
AppConfig의 등장으로 애플리케이션이 크게 사용 영역과, 객체를 생성하고 구성(Configuration)하는 영역으로 분리되었다.
FixDiscountPolicy -> RateDiscountPolicy로 변경해도 구성 영역만 영향을 받고, 사용 영역은 전혀 영향을 받지 않는다.
따라서 구성 영역인 AppConfig 부분만 수정하면 된다.
할인정책을 바꾸기 위해서는 AppConfig에서 DiscountPolicy 코드의 구현을 아래와 같이 변경하면 된다.
private DiscountPolicy discountPolicy() {
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
6. 좋은 객체 지향 설계의 5가지 원칙의 적용
-SRP 단일 책임 원칙
-> 한 클래스는 하나의 책임만 가져야 한다.
- AppConfig : 구현 객체를 생성하고 연결
- 클라이언트 객체 : 실행
-DIP 의존관계 역전 원칙
-> 프로그래머는 “추상화에 의존해야지, 구체화에 의존하면 안된다.”
- 추상화에만 의존할 수 있도록 변경
- 기존 OrderServiceImpl이 구현체에도 의존했었으나, AppConfig를 통해 구현체를 생성하도록 변경
-OCP
-> 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
- 애플리케이션을 사용 영역과 구성 영역으로 나눈다
- AppConfig가 의존관계를 FixDiscountPolicy -> RateDiscountPolicy로 변경해서 클라이언트 코드에 주입하므로 클라이언트 코드는 변경하지 않아도 된다
- 소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경을 하지 않아도 된다
7. IoC, DI, 그리고 컨테이너
1) 제어의 역전 IoC(Inversion of Control)
기존에는 구현 객체(OrderServiceImpl)이 서버 구현 객체(MemberRepository)를 생성 및 연결까지 처리했다.
반면 AppConfig 등장 이후에 구현 객체는 자신의 로직만 실행하고 프로그램 제어 흐름은 AppConfig가 가져간다.
이렇게 프로그램 제어 흐름을 직접 제어하는 것이 아닌 외부에서 제어하는 것을 제어의 역전이라 한다.
2) 의존관계주입(DI, Dependency Injection)
OrderServiceImpl은 DiscountPolicy 인터페이스에 의존한다. 실제 DiscountPolicy의 구현 객체는 어떤 것이 사용될 것인지 모른다. 의존관계는 정적인 클래스 의존관계, 실행 시점에 결정되는 동적인 객체(인스턴스) 의존관계로 나눌 수 있다.
정적인 클래스 의존관계는 애플리케이션 실행과 상관없이 클래스가 사용하는 import 코드만 보고도 의존관계를 판단할 수 있다. 아래 코드에서 알 수 있듯이 의존관계를 쉽게 판단 가능하다.
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
}
동적인 객체 인스턴스 의존 관계는 애플리케이션 실행 시점(런타임)에 생성된 객체 인스턴스의 참조가 연결된 의존관계다.
객체 다이어그램에서 보듯이 실행 시점(런타임)에 외부에서 실제 구현객체를 클라이언트에게 전달하여 클라이언트 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라고 한다.
3) IoC 컨테이너, DI 컨테이너
AppConfig와 같이 객체 생성 및 관리를 해주는 것을 IoC컨테이너, DI컨테이너라고 한다.
의존관계 주입 초점을 맞춰 주로 DI컨테이너로 부르며 오브젝트 팩토리라고도 부른다.
8. 스프링으로 전환하기
지금까지 순수 자바 코드를 만들었던 것을 스프링으로 전환해보자.
먼저 AppConfig 클래스에 @Configuration, @Bean 어노테이션을 추가하여 코드를 수정해준다.
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 FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
그 다음 프로그램 실행을 담당한 MemberApp에서 스프링 컨테이너 적용 코드로 바꾸고자 아래 코드를 추가해 수정해준다.
public class MemberApp {
public static void main(String[] args) {
// AppConfig appConfig=new AppConfig();
//MemberService memberService=appConfig.memberService();
// MemberService memberService=new MemberServiceImpl();
//AppConfig에 있는 환경 설정 정보를 스프링이 스프링 컨테이너에 다 넣어서 관리
ApplicationContext applicationContext=new AnnotationConfigApplicationContext(AppConfig.class);
//이름:memberService, type:MemberService.class
MemberService memberService=applicationContext.getBean("memberService",MemberService.class);
}
ApplicationContext를 구현한 AnnotationConfigApplicationContext 구현체를 사용하면 스프링 컨테이너에 AppConfig를 등록해 사용할 수 있다. 위와 같이 getBean()을 사용하면 내부에 등록된 빈 객체를 가져올 수 있다.
마찬가지로 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);
}
ApplicationContext 를 스프링 컨테이너라 한다.
기존에는 AppConfig만 사용하여 필요한 객체를 조회했다면, 이제 스프링 컨테이너를 사용하여 객체를 찾을 수 있다.
'Spring > 스프링 핵심 원리' 카테고리의 다른 글
인프런 스프링 기본 강의 정리 #6 (2) | 2023.11.25 |
---|---|
인프런 스프링 기본 강의 정리 #5 (0) | 2023.11.25 |
인프런 스프링 기본 강의 정리 #4 (1) | 2023.11.23 |
인프런 스프링 기본 강의 정리 #2 (0) | 2023.11.19 |
인프런 스프링 기본 강의 정리 #1 (1) | 2023.11.19 |