[스프링 핵심 원리 - 기본] 핵심 원리 이해2 - 객체 지향 원리 적용
김영한님의 스프링 핵심 원리 기본편을 들으며 정리한 글입니다.
스프링 핵심 원리 - 기본편 - 인프런 | 강의
스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...
www.inflearn.com
<새로운 할인 정책 개발>
새로운 할인 정책 확장한다. -> 애자일 소프트웨어 (계획을 따르기보다 변화에 대응한다.)
- 주문 금액당 할인하는 정률 할인으로 변경하고 싶다.
- 원래는 얼마를 주문하든 1000원이 할인되었는데 이제는 금액당 10%씩 할인하고 싶다.
RateDiscountPolicy 추가하기
DiscountPolicy의 구현체 중 RateDiscountPolicy를 추가하면 된다.
RateDiscountPolicy - discount(Member member, int price) 에서 vip이면 금액의 10% 값을 반환하도록 만들었다.
return price * discountPercent / 100;
테스트 코드 작성
@DisplayName 어노테이션은 Test할 때 각 Test의 이름으로 보여진다.
class RateDiscountPolicyTest {
RateDiscountPolicy discountPolicy = new RateDiscountPolicy();
@Test
@DisplayName("VIP는 10% 할인이 적용되어야한다")
void vip_o() {
//given
Member member = new Member(1L, "memberA", 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(1L, "memberA", Grade.BASIC);
// when
int discount = discountPolicy.discount(member, 10000);
//then
assertThat(discount).isEqualTo(0);
}
}
실행 결과
<새로운 할인 정책 적용과 문제점>
OrderServiceImpl의 코드를 고쳐야한다. -> OCP 위반 (변경하지 않고 기능을 확장하는 프로그래밍)
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
아래 그림과 같이 OrderServiceImpl은 DiscountPolicy에만 의존하는 것이 아닌 DiscountPolicy의 구현체인 FixDiscountPolicy와 RateDiscoutPolicy에도 의존하고 있다. -> DIP 위반
문제 해결 방법
인터페이스에만 의존하도록 의존관계를 변경한다.
private DiscountPolicy discountPolicy;
이렇게만 하면 Test에서 널포인터 에러가 난다. -> 누군가 OrderServiceImpl에 DiscountPolicy의 구현 객체를 생성하고 주입해야한다.
<관심사의 분리>
AppConfig의 등장
- 애플리케이션의 전체 동작 방식을 구성하기 위해 구현 객체를 생성하고 연결하는 책임을 가지는 클래스
AppConfig에서 실제 동작에 필요한 구현 객체를 생성한다.
생성한 객체 인스턴스의 참조를 생성자를 통해 주입한다.
public class AppConfig {
// 생성자 주입
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
AppConfig의 등장으로 MemberServiceImpl는 MemberRepository라는 interface에만 의존하게 된다.
MemberServiceImpl은 생성자를 통해 어떤 구현 객체가 들어올지는 알 수 없다. 이는 오직 AppConfig를 통해서 결정된다.
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
클래스 다이어그램
클라이언트인 MemberServiceImpl 입장에서는 의존관계를 외부에서 주입해주는 것 같다고 해서 DI(Dependency Injection) 의존 관계 주입 또는 의존성 주입이라고 한다.
자바 클래스에서의 테스트 코드의 변경
AppConfig를 선언하여 memberService를 부른다.
public class MemberApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
Member memberA = new Member(1L, "memberA", Grade.VIP);
memberService.join(memberA);
Member member = memberService.findMember(1L);
System.out.println("new member = " + memberA.getName());
System.out.println("find member = " + member.getName());
}
}
테스트 코드에서의 변경
@BeforeEach 어노테이션은 테스트 실행 전에 이 메서드부터 실행하게 한다.
AppConfig를 선언하여 memberService를 부른다.
public class MemberServiceTest {
MemberService memberService;
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
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(member).isEqualTo(findMember);
}
}
<AppConfig 리펙터링>
new MemoryMemberRepository() 에 중복이 있었는데 제거되었다.
AppConfig 클래스가 한 눈에 보인다. 어플리케이션 전체 구성을 빠르게 파악할 수 있다.
public class AppConfig {
// 생성자 주입
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
private MemoryMemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
<새로운 구조와 할인 정책 적용>
할인 정책 변경 시에는 AppConfig만 변경하면 된다. 사용 영역의 어떤 코드도 변경할 필요가 없다.
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
<좋은 객체 지향 설계의 5가지 원칙의 적용>
여기서 3가지 SRP, DIP, OCP 적용
SRP : 단일 책임 원칙 (하나의 클래스는 하나의 책임만 가진다)
DIP : 의존관계 역전 원칙 (추상화에 의존해야지 구체화에 의존하면 안된다)
OCP : 개방 폐쇄 원칙 (소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀있어야 한다)
<IoC, DI, 그리고 컨테이너>
제어의 역전 (Inversion of Control[IoC])
구현 객체가 프로그램의 제어의 흐름을 스스로 조절했다면, AppConfig 등장 이후 구현체는 자신의 로직을 실행하는 역할만 담당한다.
프로그램의 제어 흐름을 직접 제어하는 것이 아닌 외부에서 담당하는 것은 제어의 역전이라고 한다.
프레임워크가 내가 작성한 코드를 제어하고 대신 실행하면 그것은 프레임워크 (junit)
내가 작성한 코드가 직접 제어흐름을 담당한다면 그것은 라이브러리
의존관계 주입 (Dependency Injection[DI])
OrderServiceImpl은 DiscountPolicy 인터페이스에만 의존한다. 실제 어떤 구현체가 사용될지에 대해서는 모른다.
정적인 클래스 의존관계 - import만 보고 의존관계를 판단할 수 있다. 실행하지 않아도 분석 가능하다.
동적인 클래스 의존관계 - 어플리케이션 실행 시점에서 객체 인스턴스가 연결된 의존관계이다.
IoC 컨테이너, DI 컨테이너
AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것은 DI 컨테이너라고 한다.
또는 어셈블러, 오브젝트 팩토리 등으로 불리기도 한다.
<스프링으로 전환하기>
AppConfig의 수정
@Configuration 어노테이션을 AppConfig 클래스 위에 추가하고 각 메서드에 @Bean 어노테이션을 추가한다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemoryMemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
사용법
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = ac.getBean("memberService", MemberService.class);
OrderService orderService = ac.getBean("orderService", OrderService.class);
기존에는 AppConfig를 사용해서 직접 객체를 생성하고 DI를 했지만 이제는 스프링 컨테이너를 통해서 사용한다.
스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정 정보로 사용하고 @Bean이라 적힌 메서드를 호출하여 스프링 컨테이너에 등록한다.
이 객체를 스프링 빈이라 한다.
스프링 빈은 @Bean 이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다. ("memberService", "orderService"...등등)
스프링 빈은 applicationContext.getBean() 메서드를 이용하여 찾을 수 있다.