스프링 핵심 원리 - 기본편의 복습을 위한 글이며
이 글에 나오는 모든 소스코드의 저작원은 인프런의 김영한 강사님께 있습니다.
1. 할인 정책 적용과 문제점
이전글 비즈니스 요구사항과 설계에서 나오는 코드들을 활용하여 객체 지향 원리에 대하여 알아보겠다.
할인 정책에는 고정 할인 정책인 FixDiscountPolicy와 정률 할인 정책인 RateDiscountPolicy가 존재한다. 따라서, 원하는 할인 정책을 애플리케이션에 적용하려면 클라이언트인 OrderServiceImpl의 코드를 변경하여야 한다.
public class OrderServiceImpl implements OrderService {
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
여기서 문제점이 발생한다. 앞서 비즈니스 요구사항과 설계에서 역할과 구현을 충실하게 분리했고, 다형성을 활용하여 인터페이스와 구현 객체를 분리하였다.
그렇다면 OCP, DIP와 같은 객체 지향 설계의 원칙을 준수하였을까? 겉으로는 잘 지킨 것 처럼 보이지만, 사실 둘 다 위반하고 있다.
- 참고) 좋은 객체 지향의 설계의 5가지 원칙(SOLID) - 클릭하면 글로 이동됩니다.
1.1 DIP 위반
주문 서비스 클라이언트 OrderServiceImpl 은 DiscountPolicy 인터페이스를 의존하면서 DIP를 지킨 것 처럼 보인다. 클래스 의존 관계를 분석해보면, 추상(인터페이스) 뿐만 아니라 구현 클래스에도 의존하고 있다.
추상(인터페이스) 의존 : DiscountPolicy
구현 클래스 : FixDiscountPolicy, RateDiscountPolicy
1.2 OCP 위반
지금 코드는 기능을 확장해서 변경하면, 클라이언트 코드에 영향을 준다. 따라서 OCP를 위반한다.
1.3 해결 방법
먼저, 클래스 다이어그램으로 의존 관계를 다시 살펴보자.
먼저, [그림 1]은 개발자가 기대했던 의존 관계이고, 단순히 DiscountPolicy만 의존한다고 생각했다.
그러나 [그림 2]를 살펴보면, 실제로는 클라이언트인 OrderServiceImpl이 DiscountPolicy 인터페이스 뿐만 아니라 FixDiscountPolicy인 구체 클래스도 함께 의존하고 있다. 즉, DIP를 위반하고 있다.
[그림 3]을 살펴보면 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의 구현 객체를 대신 생성하고 주입해주어야 한다.
2. AppConfig
AppConfig는 애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스이다.
AppConfig
// AppConfig = App 전체를 설정하고 구성한다.
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
MemberServiceImpl, MemoryMemberRepository, OrderServiceImpl, FixDiscountPoliy .... 등
AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입(연결)해준다.
MemberServiceImpl → MemoryMemberRepository
OrderServiceImpl → MemoryMemberRepository , FixDiscountPolicy
지금까지는 각 클래스에 생성자가 없기 때문에 컴파일 오류가 발생한다.
2.1 생성자 주입
MemberServiceImpl
// 회원 서비스 구현체
public class MemberServiceImpl implements MemberService {
// AppConfig로 MemoryMemberRepository를 관리해주기 때문에 DIP를 지킬 수 있다.
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
설계 변경으로 인하여 더이상 MemoryMemberRepository를 의존하지 않는다. 단지 MemberRepository에만 의존한다.
MemberServiceImpl의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정된다. MemberServiceImpl은 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다.
클래스 다이어그램
객체의 생성과 연결은 AppConfig가 담당한다. 따라서 DIP가 완성되었다. 또한, 객체를 생성하고 연결하는 역할과 실행하는 역할이 완전히 분리되어 관심사의 분리도 완성되었다.
OrderServiceImpl
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;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
// 1. 주문생성요청이 들어오면
// 2. 멤버를 조회
Member member = memberRepository.findById(memberId);
// 3. 할인 정책에 회원을 넘기기
int discountPrice = discountPolicy.discount(member, itemPrice);
// 4. 최종 할인 된 가격을 받아오기
// 5. 주문을 반환
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
설계 변경으로 OrderServiceImpl은 FixDiscountPolicy를 의존하지 않는다. 단지 DiscountPolicy 인터페이스만 의존한다.
OrderServiceImpl의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정된다. OrderServiceImpl은 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다.
2.2 AppConfig 실행
MemberApp
// 회원가입 실행 해보기.
public class MemberApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
Member findMember = memberService.findMember(1L);
System.out.println("new member = " + member.getName());
System.out.println("findMember = " + findMember.getName());
}
}
OrderApp
// 주문 실행
public class OrderApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
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);
}
}
정리
AppConfig를 통해 관심사를 확실하게 분리했다. 배역과 배우를 생각해보자. AppConfig는 단지 공연 기획자의 역할을 할 뿐이다. AppConfig가 구체 클래스를 선택한다는 것은 배역에 맞는 담당 배우를 선택하는 것과 똑같다.
즉, AppConfig는 어플리케이션이 어떻게 동작해야 하는지에 대한 전체 구성을 책임지는 역할이다. 이제 각 배우들이 담당 기능을 실행하는 책임만 지면 되는 것이다. -> ex) OrderServiceImpl은 기능만 실행하는 책임을 지면 된다.
3. AppConfig 리팩토링
현재 AppConfig를 보면 중복이 있고, 역할에 따른 구현이 잘 안보인다. 중복을 제거하고, 역할에 따른 구현이 보이도록 리팩토링을 해보자.
AppConfig - 리팩토링 후
public class AppConfig {
public MemberService memberService() {
// 생성자 주입
return new MemberServiceImpl(memberRepository());
}
public MemoryMemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
AppConfig를 보면 역할과 구현 클래스가 한눈에 들어온다. 애플리케이션 전체 구성이 어떻게 되어있는지 빠르게 파악할 수 있다.
3.1 새로운 할인 정책 적용시키기
고정 할인 정책을 정률 할인 정책으로 바꾸고 싶다. 어떤 부분을 바꾸어야 할까?
FixDiscountPolicy → RateDiscountPolicy
AppConfig를 사용하여 어플리케이션이 크게 사용 영역과, 객체를 생성하고 구성(Configuration)하는 영역으로 분리되었다.
따라서, FixDiscountPolicy → RateDiscountPolicy로 변경해도 구성 영역에만 영향을 받고, 사용 영역에는 전혀 영형을 받지 않는다.
아래의 코드처럼 AppConfig를 변경해주면 된다.
package hello.core;
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public DiscountPolicy discountPolicy() {
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
4. 전체 흐름 정리
지금까지의 흐름을 정리해보면 다음과 같다.
- 새로운 할인 정책 적용과 문제점
- 관심사의 분리
- AppConfig 리팩토링
- 새로운 구조와 할인 정책 적용
4.1 새로운 할인 정책 적용과 문제점
고정 할인 정책(FixDiscountPolicy) → 정률 할인 정책(RateDiscountPolicy)으로 변경하려면 클라이언트 코드인 주문 서비스 구현체(OrderServiceImpl)도 함께 변경해야한다는 단점이 있었다.
그래서 주문 서비스 구현체(OrderServiceImpl)가 인터페이스인 DiscountPolicy뿐만 아니라, 구체 클래스인 FixDiscountPolicy까지 함께 의존을 해야 했었다. → DIP 위반
4.2 관심사의 분리
애플리케이션을 하나의 공연이라고 생각해보자. 기존에는 클라이언트가 의존하는 서버 구현 객체를 직접 생성하고 실행하였다. 비유를 하면 기존에는 남자 주인공이 배우 역할을 하여 공연도 하고, 동시에 여자 주인공도 직접 초빙하는 다양한 책임을 가지고 있었다.
공연을 구성하고, 담당 배우를 섭외하고, 지정하는 책임을 담당하는 별도의 공연 기획자가 나올 시점에 공연 기획자인 AppConfig가 등장하였다. AppConfig는 애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지고 있다.
따라서, 클라이언트 객체는 자신의 역할을 실행하는 것만 집중, 권한이 줄어들었다. (책임이 명확해짐)
4.3 AppConfig 리팩토링
구성 정보에서 역할과 구현을 명확하게 분리함으로써 역할이 잘 들어나게 되었다. → 불피요한 중복 제거!
4.4 새로운 구조와 할인 정책 적용
AppConfig의 등장으로 애플리케이션이 크게 사용 영역과, 객체를 생성하고 구성(Configuration)하는 영역으로 분리할 수 있게 되었다. 이제는 할인 정책을 변경해도 AppConfig가 있는 구성 영역만 변경하면 된다.
당연히, 사용 영역도 변경할 필요가 없다. 또한 클라이언트 코드인 주문 서비스 코드도 변경하지 않아도 된다.
잘못된 정보가 있거나 오타자가 있으면 댓글 달아주세요!
감사합니다 :)
'Backend > Spring - Core' 카테고리의 다른 글
Spring #4. 스프링 컨테이너와 스프링 빈 (0) | 2022.12.25 |
---|---|
Spring #3. 객체 지향 원리 적용 (2) (0) | 2022.12.22 |
Spring #2. 스프링 핵심 원리 기본편 - 비즈니스 요구사항과 설계 (1) | 2022.12.21 |
Spring #1. Spring 프레임워크 기본 개념 (0) | 2022.12.18 |
Spring #0. Annotation (0) | 2022.12.18 |