1. 새로운 할인 정책의 등장 → 문제 발견
2장에서 우리는 고정 할인 정책(FixDiscountPolicy)만 적용한 구조를 만들었다.
그러던 어느 날, 기획자가 다음처럼 말한다고 생각해보자.
“VIP 고객에게 결제 금액의 10%를 할인해주는 정책으로 변경하고 싶습니다!”
좋아, 그럼 우리는 새로운 정책을 하나 만들면 된다.
- FixDiscountPolicy → 기존 정책
- RateDiscountPolicy → 신규 정률 할인 정책
RateDiscountPolicy 실제 구현
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;
}
return 0;
}
}
여기까지는 아무 문제 없어 보인다.
그런데…
2. 문제 발생: DIP와 OCP가 깨졌다
정책을 변경하기 위해 OrderServiceImpl을 열어보자.
public class OrderServiceImpl implements OrderService {
private DiscountPolicy discountPolicy = **new FixDiscountPolicy();**
}
이제 정책을 10% 할인으로 변경하려면?
private DiscountPolicy discountPolicy = **new RateDiscountPolicy();**
이렇게 수정해야 한다.
여기서 심각한 문제가 드러난다.
OrderServiceImpl이 구현체를 직접 선택하고 있다!
- FixDiscountPolicy를 new 하고 있음
- RateDiscountPolicy도 직접 new 해야 함
즉, 클라이언트 코드가 구현을 알고 있다 → DIP 위반
또한 정책 변경을 위해 OrderServiceImpl을 수정해야 한다
→ 확장에는 열려 있으나 변경에는 닫혀 있어야 한다는 원칙(OCP)을 위반
결론
2장에서 분리한 “인터페이스-구현” 구조만으로는
OCP / DIP 문제를 완전히 해결할 수 없다.
3. 문제를 해결하기 위한 핵심 아이디어: 관심사의 분리
스프링의 철학과 가장 맞닿아 있는 개념이 나온다.
“구현 객체를 생성하고 연결하는 역할과
실제 비즈니스 로직을 수행하는 역할을 분리하자!”
비유로 쉽게 이해해보자
공연을 할 때, 배우들이 직접 상대 배우를 섭외하지 않는다.
- 로미오 역 배우가 “줄리엣은 누구로 할까?” 고민하지 않는다
- 감독(공연 기획자)이 모든 캐스팅을 담당한다
배우는 오직 연기(역할 수행)만 한다.
지금 우리의 구조는 배우가 상대 배우까지 직접 섭외하는 구조다.
→ 즉, OrderServiceImpl이 직접 “너 FixDiscountPolicy 해!”라고 고르고 있다.
이건 책임이 너무 많다.
4. 해결책: AppConfig 등장
이 문제를 해결하기 위해 우리는 AppConfig라는 “객체 조립 설정자”를 만든다.
- 어떤 구현체를 사용할지 선택하고
- 누가 누구에게 의존하는지 연결하고
- 객체를 생성하는 책임을 모두 맡는다
AppConfig 코드
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 RateDiscountPolicy(); // 정책 변경
}
}
AppConfig는 다음 3가지 책임을 가진다.
- 객체 생성
- 의존관계 연결
- 구현체 선택
AppConfig가 등장하면서 객체를 관리하는 방식이 다음처럼 바뀌었다.
- MemberServiceImpl은 MemberRepository를 직접 선택하지 않음
- OrderServiceImpl은 DiscountPolicy를 직접 선택하지 않음
- 오직 AppConfig가 모든 선택을 담당
- 클라이언트는 오로지 “역할”만 바라봄
5. DIP / OCP 완성
DIP
객체는 오직 인터페이스(추상)에만 의존한다.
구현체는 AppConfig가 대신 선택하기 때문에
Service는 구현(구체 클래스)을 전혀 모른다.
OCP
할인 정책을 바꾸고 싶다면?
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
정책 교체는 오직 AppConfig만 하나 바꾸면 된다.
OrderServiceImpl 등 나머지 코드는 전혀 변경되지 않는다.
→ 확장에는 열려 있고, 변경에는 닫혀 있는 구조 완성!
6. 전체 흐름 정리
객체 생성과 연결 흐름
AppConfig ---------> MemberServiceImpl ------> MemberRepository
\\
\\------> OrderServiceImpl --------> MemberRepository
\\-> DiscountPolicy
서비스 코드에서는 이렇게 사용한다.
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
OrderService orderService = appConfig.orderService();
이제 Service는 구현체를 new 하지 않는다.
오직 AppConfig에서 생성된 인스턴스를 주입받아서 사용한다.
7. 스프링 컨테이너로 가기 직전 단계
지금까지 우리가 하고 있는 것은 사실 스프링이 자동으로 해주는 일이다.
- 객체 생성
- 의존관계 연결
- 정책 선택
- Singleton 관리
이 모든 작업을 개발자가 직접 AppConfig에서 하고 있다.
3장은 “스프링이 어떤 문제를 해결하는지”를 코드로 직접 체험하는 과정이며
4장부터는 “이제 이것을 스프링 컨테이너에게 맡기자!”로 넘어가게 된다.
즉, AppConfig = 스프링 컨테이너(의 축소판)
3장 핵심 요약
- 새로운 정책 등장 → OrderServiceImpl 수정 필요 → OCP / DIP 위반 발생
- 책임 분리 필요 → “객체 생성/연결”과 “실행”을 분리해야 함
- AppConfig 등장 → 모든 객체 생성과 의존관계를 관리
- DIP / OCP를 만족하는 완전한 객체 지향 구조 완성
- 다음 단계는 “스프링 컨테이너로 옮기기”