김영한씨의 스프링 핵심 원리 - 기본편 강의를 듣고 공부 겸 정리하는 글입니다.
기존에 변경될 수도 있다고 이야기한 할인 정책이 갑자기 바뀌었다고 생각해봅시다.
그렇다면 새로운 할인 정책이 기존에 만들어 놓은 할인 정책 대신에 들어가야 해요.
객체 지향 설계 원칙을 준수 했다면 큰 어려움이 없다!
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;
}
}
}
VIP 회원일 때 10%를 할인해주는 정책입니다.
public class RateDiscountPolicyTest {
RateDiscountPolicy discountPolicy = new RateDiscountPolicy();
@Test
@DisplayName("VIP는 10% 할인이 적용")
void vip_o() {
Member member = new Member(1L, "memberA", Grade.VIP);
int discount = discountPolicy.discount(member, 10000);
assertThat(discount).isEqualTo(1000);
}
@Test
@DisplayName("VIP 아니라면 할인 적용 x")
void vip_x() {
Member member = new Member(1L, "member2", Grade.BASIC);
int discount = discountPolicy.discount(member, 10000);
assertThat(discount).isEqualTo(0);
}
}
@DisplayName 어노테이션은 테스트 클래스와 메소드의 이름을 붙여주는 역할을 합니다.
위와 같이 변경하면 무엇이 문제일까?
이렇듯, 할인 정책을 새롭게 변경하게 되면 할인 정책을 이용하는 OrderServiceImpl 역시 변경되어야 합니다. 아래처럼 말이지요.
//private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
역할(인터페이스)과 구현(구현체)을 잘 구분 했고, DIP, OCP 같은 객체 지향 설계 원칙을 잘 준수했다고 보여요. (정말 ?!)
하지만!!
기대한 의존관계
실제 의존관계
ServiceImpl(클라이언트)의 입장에서는 역할을 담당하는 DiscountPolicy에만 의존하는 것이 아니라 FixDiscountPolicy, RateDiscountPolicy도 알고 있어야 하므로, 구현체에도 의지한다고 할 수 있답니다.
그렇기에 구현체를 변경하면 클라이언트의 코드도 변경해야하므로 OCP도 위반하고 있다는 사실..!
결국, 우리가 해야할 일은 클라이언트가 인터페이스에만 의존하도록 변경하는것
ublic class OrderServiceImpl implements OrderService {
//private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private DiscountPolicy discountPolicy;
}
이 상태 그대로 실행하면 구현체가 없으니 Null 포인터 오류가 나게 됩니다.
그래서 누군가가 구현 객체를 대신 생성해서 주입해주어야 합니다.
어플리케이션이 하나의 공연이라고 생각해보겠습니다.
각각의 인터페이스를 배우 역할이라고 한다면, 그 역할에 맞는 사람을 고르는 것은 누가 하게 될까요?!
드라마 그 해 우리는에서 국연수(김다미) 역할을 최웅(최우식)이 정할까요? 아닙니다. 바로 연출자, 감독이 정하게 되겠죠.
이전까지는 국연수 역할을 최우식이 정하고 있었답니다. 즉, 배우의 책임이 증가했다는 이야기죠.
결국, 관심사를 분리하자는 이야기!
이를 가능하게 하기 위해, 어플리케이션의 전체 동작 방식을 구성(config)하는 파일을 생성해줍니다. 구현 객체를 설정하고, 연결하는 책임을 가진 클래스입니다.
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
AppConfig는 생성한 객체 인스턴스의 참조를 생성자를 통해서 주입합니다.
이렇게 되면 기존에 작성한 다른 파일들에 생성자를 추가해야 합니다.
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
이렇게 되면 MemerServiceImpl은 구현체를 의존하지 않고, 인터페이스인 MemberRepository만 의존하게 됩니다.
AppConfig가 MemoryMemberRepository 객체를 생성해 그 참조를 MemberSerivceImpl을 생성하면서 주입하게 됩니다.
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
동일하게 바꿔 줍니다.
MemberApp
public class MemberApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
... 동일
}
OrderApp
public class OrderApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
OrderService orderService = appConfig.orderService();
... 동일
}
MemberServiceTest
public class MemberServiceTest {
MemberService memberService;
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
}
... 동일
}
OrderServiceTest
public class OrderServiceTest {
MemberService memberService;
OrderService orderService;
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
}
}
@BeforeEach 어노테이션은 테스트 실행하기 전에 호출됩니다.
결국 AppConfig라는 공연 기획자가 모든것(구현체)을 결정하며
어플리케이션이 어떻게 돌아갈지 전체적인 책임을 지게 된다.
각 클라이언트는 담당 기능만 실행하는 책임을 지면 된다.
현재는 중복도 있고 역할에 따른 구현이 잘 보이지 않습니다.
new MemoryMemberRepository()가 두개 있으며,
MemoryMemberRepository의 경우 역할인 MemberRepository가 서로 구분되지 않아요.
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();
}
}
중복을 지워주고 보니 역할과 구현도 확실히 분리됩니다.
만약 할인 정책을 다시 바꾼다고 한다면 new FixDiscountPolicy() 부분만 새롭게 바꿔주면 된답니다.
어플리케이션을 사용하는 영역과 구성하는 영역(Configuration)으로 나누어졌습니다.
또한, 앞으로는 AppConfig만 변경하고 클라이언트 코드를 전혀 손볼 필요가 없어졌습니다.
클라이언트측에서 생성하고 연결하고 실행하는 것은 매우 자연스러운 일입니다.
다만, 앞에서 했듯이 모든 제어권을 이제 클라이언트가 아닌 AppConfig에서 관리하게 됩니다.
클라이언트는 오직 자신의 로직만 수행할 뿐이죠. 이러한 현상을 제어의 역전이라고 부릅니다.
정적인 클래스 의존 관계와 동적인 클래스 의존 관계 두가지가 존재합니다.
정적인 클래스 의존 관계
import문을 보고 바로 의존관계를 쉽게 파악할 수 있습니다. 즉, 실행시키지 않아도 확인할 수 있다는 이야기에요.
가령, OrderServiceImpl은 MemberRepository, DiscountPolicy에 의존한다는 것을 알 수 있는 것처럼 말이에요.
다만, 이것만 보고 실제로 어떤 구현체가 주입될지는 알 수 없답니다.
동적인 클래스 의존 관계
앱이 실행된 후에야 의존 관계를 파악할 수 있습니다. 외부에서 객체를 생성해서 클라이언트에 전달하고 서로 의존 관계가 연결 됩니다.
AppConfig처럼 객체를 생성, 관리하면서 의존 관계를 연결해주는 것을 DI컨테이너라고 부릅니다.
지금까지는 순수한 자바코드로 DI 적용을 했다면, 이제는 스프링으로 해보겠습니다!
AppConfig
@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();
}
}
MemberApp
public class MemberApp {
public static void main(String[] args) {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = ac.getBean("memberService", MemberService.class);
... 동일
}
OrderApp
public class OrderApp {
public static void main(String[] args) {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = ac.getBean("memberService", MemberService.class);
OrderService orderService = ac.getBean("orderService", OrderService.class);
... 동일
}
코드가 더 복잡해 보이는데 어떤 장점이 있는걸까요?
다음 글에서 만나겠습니다...