새로운 할인 정책 개발
새로 나온 정책은 10%로 지정해두면 고객이 10000원 주문 시 1000원을 할인해주고, 20000원 주문 시에 2000원을 할인해주는 것.
@Test
@DisplayName("VIP는 10% 할인이 적용되어야 한다")
void vip_o(){
//given
Member member = new Member(1L, "memberA", Grade.VIP);
//when
int discount =discountPolicy.discount(member,10000);
System.out.println("discountPolicy = " + discountPolicy);
//then
Assertions.assertThat(discount).isEqualTo(1000);
}
// 아닐 경우의 테스트를 적용해 보는 것도 중요하다.
@Test
@DisplayName("VIP가 아니면 할인이 적용되지 않아야 한다.")
void vip_x(){
//given
Member member = new Member(1L, "memberA", Grade.BASIC);
//when
int discount =discountPolicy.discount(member,10000);
System.out.println("discountPolicy = " + discountPolicy);
//then
Assertions.assertThat(discount).isEqualTo(1000);
}
- 실제 실무에서는 돈에 관련된 부분에선 더욱 민감하게 테스트를 한다.
할인 정책 변경
- 문제점
- OCP, DIP 같은 객체지향 설계 원칙을 충분히 준수하지 않았다.
- memberRepository의 경우 추상체 뿐만 아니라 구현체 또한 의존하고 있다. → DIP 위반
- policy를 변경하는 순간 OrderServiceImpl을 변경해야한다. → OCP 위반
- OCP, DIP 같은 객체지향 설계 원칙을 충분히 준수하지 않았다.
- 해결법
- 인터페이스만 의존하도록 코드를 변경한다.
- 이를 위해서는 서비스의 책임을 분리하는 것이다.
- 인터페이스만 의존하도록 코드를 변경한다.
- 위 코드는 객체 주입이 되어있지 않고 있기 때문에 NullPointException이 발생한다. → 객체 주입에 대한 책임을 분리한다.
관심사 분리
- 지금까지 작성한 코드는 배우가 상대 배우를 초청하는 공연 기획자 역할을 겸임 한 것과 같다.
- 따라서 공연 기획자를 따로 만들고, 배우와 공연 기획자의 책임을 분리한다. (객체 주입 - 배우 섭외)
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(
new MemoryMemberRepository(),new FixDiscountPolicy());
}
}
- 생성자를 주입을 통해서 Service는 의존 관계에 대한 책임을 모두 외부(AppConfig)에 맡기게 된다.
- 클래스 다이어그램
- 위 그림처럼 AppConfig가 MemberServiceImpl과 MemoryMemberRepository 생성한다.
- MemberServiceImpl은 생성자 주입이 완료되면 meberRepository만 의존하게 되며 DIP,OCP를 준수할 수 있게된다.
- 회원 객체 인스턴스 다이어그램
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
- 마침내 memeberServiceImpl은 기능 실행에만 집중하면 된다.
구체 클래스(구조체)에 의존하지 않는 이유:
- 유연성 감소: 구체 클래스에 의존하면, 해당 클래스의 구현에 강하게 결합되어 변경이 어렵다. 새로운 기능을 추가하거나 다른 구현으로 변경할 때 전체 코드를 리팩토링해야 할 수 있습니다.
- 테스트 어려움: 구체 클래스에 의존하는 경우, 단위 테스트를 작성하기 어려울 수 있다. 특히 외부 시스템에 의존하는 클래스의 경우 모의 객체를 사용하기 어렵다.
- 코드의 강한 결합: 구체 클래스에 의존하면 코드가 특정 구현에 강하게 결합되어, 시스템의 다른 부분이 해당 클래스의 구현에 의존하게 된다. 이는 유지보수를 어렵게 만든다.
DIP를 준수해야 하는 이유:
- 유연성과 확장성: 인터페이스에 의존하면 구현체가 변경되어도 해당 인터페이스를 사용하는 코드는 수정할 필요가 없다. 이를 통해 새로운 기능을 추가하거나 다른 구현체로 교체하는 것이 용이해진다.
- 낮은 결합도: 클래스가 구체 클래스가 아닌 인터페이스에 의존하면 결합도가 낮아진다. 따라서서 유지보수를 용이하게 한다.
- 테스트 용이성: 인터페이스에 의존하면 모의 객체(Mock)나 스텁(Stub)을 사용하여 테스트를 용이하게 할 수 있다.
- 코드 재사용성: 인터페이스에 의존하면 동일 인터페이스를 구현하는 다양한 클래스들과의 호환성을 유지할 수 있으므로 코드 재사용성이 향상된다.
스프링 bean과 container를 이용한 결과
실행 시 container에 등록된 것을 볼 수 있음.
IoC, DI, 그리고 컨테이너
IoC(Inversion of Control) 제어의 역전
- 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것
- 내가 호출하는게 아니라 프레임워크가 대신 호출해주는 것.
- 구현 객체가 프로그램의 제어 흐름을 스스로 조종하지 않고 그저 호출 받으면 실행하는 “역할만 담당한다”.
프레임워크 vs 라이브러리
- 프레임워크가 내가 작성한 코드를 제어한고 그 코드를 나 대신 실행하면 그것은 프레임워크다. (JUnit)
- 냐거 작성한 코드의 제어흐름을 직접 관리한다면 그것을 라이브러리다.
JUnit은 프레임워크인데 왜 JUnit의 라이브러리를 추가해서 사용한다고 하는건가?
- JUnit이라는 프레임워크를 사용하기 위한 추가적인 코드 모음을 클라이언트가 직접 제어해서 사용하기 때문이다.
- 프레임워크는 "프레임"이라는 말처럼 틀을 만들어두고 개발자로 하여금 사용 방법을 강제하는 것에 반해, 라이브러리는 개발자가 좀 더 자유롭게 사용할 수 있는 코드 조각 모음이다.
- 프레임워크는 "프레임"이라는 말처럼 틀을 만들어두고 개발자로 하여금 사용 방법을 강제하는 것에 반해, 라이브러리는 개발자가 좀 더 자유롭게 사용할 수 있는 코드 조각 모음이다.
의존 관계 주입 DI(Dependency Injection)
- 구현체는 인터페이스에 의존한다. 구현체는 그 자신이 사용될지 안될지 알지 못한다 → 실제 어떤 구현체가 사용될지는 모른다.
- 의존관계는 ‘정적인 클래스 의존 관계와, 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계” 둘을 분리해서 생각해야한다.
동적인 객체 의존 관계
- 구현체는 그 자신이 사용될지 안될지 알지 못한다 → 실제 어떤 구현체가 사용될지는 모른다.
- 실제로 애플리케이션이 실행될 때마다 주입되는 인터페이스가 달라질 수 있음
- 애플리케이션 런타임에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라고 한다.
- 를 통해 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.
- 정적인 클래스 의존관계를 변경하지않고, 동적인 객체 인스턴스 의존계를 쉽게 변경할 수 있다.
IoC 컨테이너, DI 컨테이너
- AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 “IoC 컨테이너, DI 컨테이너”라고 한다.
- 어샘블러, 오브젝트 팩토리라고도 한다.
스프링으로 전환
@Configuration // 설정 정보 , 구성 정보
public class AppConfig {
@Bean // 스프링 컨테이너에 등록된다.
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public static MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(
memberRepository(),discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
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("member = " + member.getName());
System.out.println("findMember = " + findMember.getName());
}
}
- ApplicationContext를 스프링 컨테이너라고 한다.
- 기존에는 개발자가 AppConfig를 사용해서 직접 DI를 했지만, 스프링 컨테이너를 이용해 자동화한다.
- 스프링 컨테이너는 @Configuration이 붙은 ‘AppConfig’를 설정 정보로 사용한다. 여기서는 @Bean으로 등록된 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다.
- → 이를 스프링 빈이라고 한다.
- 이 스프링 빈을 꺼내 사용하려면 applicationContext.getBean을 사용하자.