1. 새로운 할인 정책 개발
원래는 고정 할인 정책이었지만, 기획자가 갑자기 정률 할인으로 바꾸자고 한다.
하지만 괜찮다! 우리는 객체 지향 원칙에 맞게 구현했기 때문이다.
이미 DiscountPolicy 인터페이스가 있으니 이걸 가지고 RateDiscountPolicy 클래스를 구현하면 된다.
DiscountPolicy 인터페이스
public interface DiscountPolicy {
int discount(Member member, int price);
}
RateDiscountPolicy 구현 클래스
VIP이면 10% 할인을 해주는 `discount()`를 만든다.
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;
}
}
}
테스트
RateDiscountPolicy 클래스에 커서를 대고,
`cmd + shift + T`로 새로운 테스트 만들기를 클릭해서 올바른 패키지에 테스트 파일을 자동으로 만든다.
VIP인 경우의 테스트(`vip_o()`)와 BASIC인 경우 테스트(`vip_x()`)를 따로 할 수 있다.
BASIC인 경우는 할인 금액이 없으므로 `isEqualTo(0)`으로 비교해야한다.
class RateDiscountPolicyTest {
RateDiscountPolicy discountPolicy = new RateDiscountPolicy();
@Test
@DisplayName("VIP는 10% 할인이 적용되어야 한다.")
void vip_o(){
// given
Member member = new Member(1L,"memberVIP", Grade.VIP);
//when
int discount = discountPolicy.discount(member, 10000);
// then
assertThat(discount).isEqualTo(1000); // 1000원 할인
}
@Test
@DisplayName("VIP가 아니면 할인이 적용되지 않아야 한다.")
void vip_x(){
// given
Member member = new Member(2L,"memberBASIC", Grade.BASIC);
//when
int discount = discountPolicy.discount(member, 10000);
// then
assertThat(discount).isEqualTo(0); // 0원 할인
}
}
`@DisplayName`으로 테스트 이름을 지정해줄 수 있다.
2. 새로운 할인 정책 적용과 문제점
OrderServiceImpl 에서 새로운 할인 정책을 적용해야한다.
문제 1 : DIP 위반
이 서비스 구현 클래스는 추상화(discountPolicy)에도 의존하고, 구체화(RateDiscountPolicy)에도 의존하고 있다. (코드를 알고 있다.)
DIP를 만족하려면 인터페이스에만 의존해야 한다.
// OrderServiceImpl 에서 ..
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
문제 2 : OCP 위반
기존의 Fix 할인 정책에서 새로운 Rate 할인 정책으로 코드를 바꿔주어야한다.
OCP는 변경하지 않고 확장할 수 있다고 했는데, 코드 변경이 필요하므로 위반한 것이다.
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
// ⬇️
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
그래서 어떻게 해결할 수 있을까?
인터페이스에만 의존하게 하면 DIP도 해결되고, 그러므로 구체화에 의존할 필요가 없으므로 코드 변경도 필요가 없어서 OCP 문제도 해결된다.
private DiscountPolicy discountPolicy;
그런데 아직 해결은 안됐다.
DiscountPolicy 인터페이스만 있고 어떤 구현체가 실행되어야 하는지 결정이 안되어있기 때문이다.
따라서 누군가 클라이언트인 OrderServiceImpl에 DiscountPolicy의 구현 객체를 대신 생성하고 주입해주어야 하는 것이다.
3. 관심사의 분리
AppConfig를 통해서 관심사를 분리해야한다.
배역과 배우를 생각해보면, 로미오 역할 배우는 줄리엣 역할 배우를 섭외하지 않는다. 이렇게 되면 배우의 책임이 너무 커진다.
공연을 총괄하는 기획자가 배우를 섭외하는 것처럼 AppConfig 라는 기획자가 애플리케이션이 어떻게 동작해야할지 결정하게 하는 것이다.
그럼 이제 각 배우들(`SeviceImpl`)은 담당 기능을 실행하는 책임만 지면 된다.
객체를 생성하고 연결하는 역할과 실행하는 역할이 명확이 분리된다. (관심사의 분리)
1. AppConfig
애플리케이션의 전체 동작 방식을 구성하기 위해, 구현 객체를 생성하고 연결하는 책임을 가지는 별도의 설정 클래스.
어떤 구현체가 들어갈 것인지는 여기서 결정하고, ServiceImpl에서는 결정된 구현체를 받아서 쓰면 되므로 ServiceImpl에 생성자가 필요해졌다.
memberService는 MemoryMemberRepository 구현체를 사용할 것이고, OrderServiceImpl 은 FixDiscountPolicy 구현체를 사용하려고 한다. AppConfig에서 의존성을 주입한다.
// 생성한 객체 인스턴스의 참조를 생성자를 통해서 주입(연결)해준다.
public class AppConfig {
// 어떤 구현체가 들어갈지 여기서 결정한다.
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService(){
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
2. ServiceImpl
기존의 구현체를 직접 받아서 썼던 코드를 지우고, AppConfig에서 받은 구현체를 사용하기 위해 생성자를 만든다.
구현체에 직접 의존하지 않아 이제부터 의존관계에 대한 고민은 외부에 맡기고, 서비스는 실행에만 집중하면 된다.
2-1. MemberServicImpl
// 삭제: private final MemberRepository memberRepository = new MemoryMemberRepository();
private final MemberRepository memberRepository;
// 샐성자가 필요해졌다: AppConfig에서 어떤 구현체를 넣을건지 결정해주기 때문.
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
2-2. OrderServiceImpl
// DIP를 지킨다. 이 인터페이스로 어떤 구현체가 들어올지 나는 모른다.
private final MemberRepository memberRepository; // 리포지토리에서 회원을 찾고,
private final DiscountPolicy discountPolicy; // 할인 서비스에 회원을 넘긴다.
// 생성자가 필요해졌다.
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
3. 어플리케이션 파일 수정
AppConfig에서 memberService를 받아와서 사용한다.
3-1. MemberApp
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService(); // appConfig에서 의존성 주입이 이루어진다.
// 삭제: MemberService memberService = new MemberServiceImpl();
3-2. OrderApp
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
OrderService orderService = appConfig.orderService();
// 삭제: MemberService memberService = new MemberServiceImpl();
// 삭제: OrderService orderService = new OrderServiceImpl();
4. 테스트 코드 수정
4-1. MemberServiceTest
`@BeforeEach` : 각 테스트 실행 전에 무조건 실행되게 한다.
테스트 전에 의존성을 주입받아서 `join()`함수를 실행할 수 있게 된다.
MemberService memberService;
@BeforeEach // 각 테스트 실행 전에 무조건 실행된다.
public void beforeEach(){
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
}
// 삭제: MemberService memberService = new MemberServiceImpl();
@Test
void join() { ... }
4-2. OrderServiceTest
MemberService memberService ;
OrderService orderService ;
@BeforeEach // 각 테스트 실행 전에 무조건 실행된다.
public void beforeEach(){
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
}
4. AppConfig 리펙토링
5. 새로운 구조와 할인 정책 적용
6. 전체 흐름
7. 좋은 객체 지향 설계의 5가지 원칙의 적용
8. Ioc, DI, 그리고 컨테이너
9. 스프링으로 전환하기
퀴즈
1. 현재 MemberServiceImpl과 OrderServiceImpl은 MemoryMemberRepository와 FixDiscountPolicy라는 구체 클래스에 직접 의존하고 있다. 이는 어떤 객체지향 원칙을 위반하며, 이로 인해 발생하는 가장 큰 문제점은 무엇인가?
private final MemberRepository memberRepository = new MemoryMemberRepository();
MemberServiceImpl과 OrderServiceImpl이 각각 MemoryMemberRepository와 FixDiscountPolicy라는 구체 클래스에 직접 의존하는 것은 **DIP(Dependency Inversion Principle, 의존 역전 원칙)**를 위반한다. DIP는 "추상화에 의존해야 하며, 구체화에 의존해서는 안 된다"는 원칙이다. 즉, 상위 모듈은 하위 모듈의 구체적인 구현이 아닌 추상화(인터페이스)에 의존해야한다.
2. MemberServiceImpl의 현재 구조는 새로운 요구사항, 예를 들어 MemoryMemberRepository나 JpaMemberRepository와 같은 다른 종류의 회원 저장소로 변경될 때 어떤 어려움을 발생시킬까? 이 어려움을 해결하기 위한 가장 기본적인 객체지향 설계 원칙은 무엇인가?
MemberServiceImpl는 MemoryMemberRepository라는 구체 클래스를 직접 생성하고 의존하고 있다.
- OCP (개방-폐쇄 원칙) 위반: OCP는 "확장에는 열려있고, 변경에는 닫혀있어야 한다"는 원칙이다. 새로운 저장소가 추가될 때마다 기존 MemberServiceImpl의 코드를 변경해야 하므로 OCP를 위반한다.
- DIP (의존 역전 원칙) 위반: MemberServiceImpl이 MemberRepository (추상화)에 의존하는 동시에 MemoryMemberRepository (구현체)에도 의존하고 있다. 즉, 상위 모듈(서비스)이 하위 모듈(구현체)에 의존하고 있어 DIP를 위반한다.
3. 애플리케이션의 전체 동작 방식을 구성하기 위해, 구현 객체를 생성하고 연결하는 책임을 가지는 별도의 설정 클래스는 무엇인가?
AppConfig 클래스.
애플리케이션의 전체 동작 방식을 구성하기 위해, 구현 객체(예: MemoryMemberRepository, FixDiscountPolicy, MemberServiceImpl, OrderServiceImpl 등)를 생성하고 이들 객체 간의 의존 관계를 연결하는 책임을 가지는 별도의 설정 클래스는 일반적으로 AppConfig (애플리케이션 설정) 또는 Configuration 클래스라고 부른다.
'[Web-Back] Spring > Study' 카테고리의 다른 글
스프링 핵심 원리의 이해 1 - 회원의 주문과 할인 (1) | 2025.07.07 |
---|---|
객체 지향 설계와 스프링 (3) | 2025.06.26 |