역할과 구현 분리
역할과 구현을 분리해서 자유롭게 구현 객체를 조립할 수 있도록 설계한 클래스 다이어그램
회원을 메모리에서 조회하고, 정액 할인 정책(고정 금액)을 지원해도 주문 서비스를 변경하지 않아도 된다.
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository = new
MemoryMemberRepository();
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
주문 생성 요청이 오면, 회원 정보를 조회하고, 할일 정책을 적용한 다음 주문 객체를 생성해서 반환
메모리 회원 리포지토리와 고정 금액 할인 정책을 구현체로 생성
-> 새로운 정책이 추가된다면 다음 코드와 같이 OrderServiceImpl 코드를 고쳐야한다
public class OrderServiceImpl implements OrderService {
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
여기서 기존에 만든 코드가 어떤 SOLD원칙을 위반하고 있는지 살펴보자
기존 코드에서 위반된 SOLID원칙
- 역할과 구현을 분리 -> 구현 객체를 자유롭게 조립
- 다형성 활용, 인터페이스와 구현 객체 분리 -> DiscountPolicy 인터페이스를 활용해서 RateDiscountPolicy 구현
- OCP(Open Closed Policy), DIP(Dependency Inversion Policy) 같은 객체 지향 설계 원칙을 준수한 것처럼 보이지만 그렇지 않음 -> OrderServiceImpl이 DiscountPolicy(인터페이스)와 RateDiscountPolicy(구현체) 를 의존하고 있음.
OCP를 지키기 위해서
할인 정책이 변경되어도 OrderServiceImpl의 코드가 변경되면 안된다.
-> OrderServiceImpl은 DiscountPolicy(인터페이스)만을 의존하도록 변경
DIP를 지키기 위해서
고수준 모듈이이 저수준 모듈을 참고할 때 인터페이스를 참고해야한다. 즉 OrderServiceImpl(고수준)이 할인 정책(저수준)을 참고할 때 DiscountPolicy(인터페이스)를 참고해야한다는 말이다.
고수준 모듈은 저수준 모듈의 구현에 의해 의존해서는 안되며, 저수준 모듈이 고수준 모듈에 의존해야한다는 것이다
근데 지금은 OrderSerivceImpl(고수준)이 구현체인 RateDiscountPolicy에 의존하고 있다.
OCP와 DIP를 지키기 위해서 인터페이스에만 의존하도록 설계하면 된다.
public class OrderServiceImpl implements OrderService {
//private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private DiscountPolicy discountPolicy;
}
그럼 여기서 구현체가 없는데 어떻게 코드를 실행할 수 있는가?
-> 누군가가 클라리언트인 OrderServiceImpl에 DiscountPolicy의 구현 객체를 대신 생성하고 주입해주어야한다.
⭐️관심사의 분리
public class OrderServiceImpl implements OrderService {
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
위 코드에서 OrderServiceImpl은 DiscountPolicy(인터페이스)가 어떤 구현체(FixDiscountPolicy, RateDiscountPolicy)를 사용할직 직접 결정하고 있다.
OderServiceImpl은 DiscountPolicy(인터페이스) 만 의존하도록 하고, AppConfig를 생성해서 아래 코드에서 DiscountPolicy(인터페이스)에게 구현체를 할당해주도록 하자 -> 구현제를 만들고 주입하는 역할(AppConfig), 구현체를 사용하는 역할(OrderServiceImpl)을 분리
public class OrderServiceImpl implements OrderService {
//private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private DiscountPolicy discountPolicy;
}
구현체 생성 + 주입 하는 역할
애플리케이션의 전체 동작 방식을 구성(Config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스를 만들자
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(
new MemoryMemberRepository(),
new FixDiscountPolicy());
}
}
[구현 객체 생성]
- new OrderServiceImpl
- new MemoryMemberREpository
- new FixDiscountPolicy
- new MemverServiceImpl
[생성자를 통해 사용할 객체 주입]
- MemberServiceImpl 생성자를 통해 MemoryMemberRepository 주입
- OrderServiceImpl 생성자를 통해 MemeoryMemberRepository, FixDiscountPolicy 주입
수정된 클라이언트 코드(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;
}
}
OrderServiceImple은 인터페이스(MemberRepository, DiscountPolicy) 만 의존하고 구현체를 의존하지 않는다.
AppConfig 리팩터링
현재 AppConfig는 중복이 있고, 역할에 따른 구현이 잘 안보인다.
아래 코드와 같이 중복을 제거하고, 역할에 따른 구현이 보이도록 리팩터링
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();
}
}
- new MemoryMemberRepository 이 부분이 중복 제거, 이제 MemeoryMemberRepository를 다른 구현체로 변경할 때 한부분만 변경하면 된다.
- AppConfig를 보면 역할과 구현 클래스가 한눈에 들어온다. 애플리케이션 전체 구성이 어떻게 되었는지 빠르게 파악 가능
관심사 분리 후 적용된 SOLID 원칙
SRP, DIP, OCP 적용
1. SRP(Single Resposibility Policy)
한 클래스는 하나의 책임만 가져야한다.
클라이언트 객체는 직접 구현 객체를 생성하고, 연결하고, 실행하는 다양한 책임을 가지고 있었다.
- 구현 객체를 생성하고 연결하는 책임은 AppConfig 가 담당
- 클라이언트 객체(OrderServiceImpl)는 실행하는 책임만 담당
2. DIP (Dependency Inversion Policy)
프로그래머는 추상화에 의존해야지, 구체화에 의존해서는 안된다. -> 의존성 주입을 이 원칙을 따르는 방법 중 하나이다.
[기존코드 문제점]
public class OrderServiceImpl implements OrderService {
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
새로운 할일 정책을 개발하고, 적용하려고 하니 클라이언트 (OrderServiceImpl) 코드도 함께 변경해야 했다.
[DIP 준수하면서 수정한 코드]
public class OrderServiceImpl implements OrderService {
//private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private DiscountPolicy discountPolicy;
}
클라이언트 코드가 DiscountPolicy 추사오하 인터페이스에만 의존하도록 코드 변경
구체화는 어디서 주입해주나? -> AppConfig가 대신 구체화 클래서 생성하고 주입해주도록 함
3. OCP (Open Closed Policy)
소프트웨어 요소는 확장에 열려 있으나 변경에 닫혀 있어야한다.
- 다형성(인터페이스)을 사용하고 클라이언트가 DIP(인터페이스만 의존)를 지킴
- 애플리케이션을 사용 영역(OrderServiceImpl)과 구성 영역(AppConfig)으로 나눔
- AppConfig가 의존과계를 FixDiscountPolicy -> RateDiscountPolicy 로 변경해서 클라이언트 코드에 주입하므로 클라이언트 코드는 변경하지 않아도 됨
- 소프트웨어 요소를 새롭게 확장해소 사용 영역의 변경은 닫혀 있다.
'Spring > Spring 핵심 원리' 카테고리의 다른 글
[Spring] 싱글톤 컨테이너, @Configuration과 바이트코드 조작 (3) | 2024.11.07 |
---|---|
[Spring] Ioc, DI, 컨테이너 (0) | 2024.11.07 |
빈 생명주기 콜백 (0) | 2023.09.18 |
자동 빈 등록 vs. 수동 빈 등록 - 실무 운영 기준 (0) | 2023.09.17 |
조회한 빈이 모두 필요할 때 List, Map (0) | 2023.09.17 |