이 글은 스프링 핵심 원리 - 기본편 강의를 듣고 정리한 내용입니다.
정리 : 회원 데이터, 할인 정책은 아직 명확히 정해지지 않았다. 그렇다고 정책이 결정될 때 까지 개발을 기다릴 수 없다‼️ 이때 구현과 역할을 분리해서 이 문제를 해결할 수 있다. 즉, 인터페이스를 만들고 구현체를 언제든지 갈아끼울 수 있도록 설계하는 것이다.
회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. → 미확정
🌈 MemberRepository 인터페이스를 만들고 MemoryMemberRepository와 DbMemberRepository를 구현체로 설계한다!

할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 않았다. → 미확정
🌈 Discount 인터페이스를 만들고 FixDiscountPolicy와 RateDiscountPolicy를 구현체로 설계한다!

🌈 Grade
public enum Grade {
BASIC,
VIP
}
🌈 Member
public class Member {
private Long id;
private String name;
private Grade grade;
public Member(Long id, String name, Grade grade) {
this.id = id;
this.name = name;
this.grade = grade;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Grade getGrade() {
return grade;
}
public void setGrade(Grade grade) {
this.grade = grade;
}
}
🌈 MemberRepository
public interface MemberRepository {
void save(Member member);
Member findById(Long memberId);
}
🌈 MemoryMemberRepository
public class MemoryMemberRepository implements MemberRepository{
private static Map<Long, Member> store = new HashMap<>();
@Override
public void save(Member member) {
store.put(member.getId(), member);
}
@Override
public Member findById(Long memberId) {
return store.get(memberId);
}
}
🌈 MemberService
public interface MemberService {
void join(Member member);
Member findMember(Long memberId);
}
🌈 MemberServiceImpl
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
public void join(Member member) {
memberRepository.save(member);
}
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
🌈 DiscountPolicy
public interface DiscountPolicy {
//할인된 가격 반환
int discount(Member member, int price);
}
🌈 FixDiscountPolicy
public class FixDiscountPolicy implements DiscountPolicy {
private int discountFixAmount = 1000; //1000원 할인
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return discountFixAmount;
} else {
return 0;
}
}
}
🌈 Order
public class Order {
private Long memberId;
private String itemName;
private int itemPrice;
private int discountPrice;
public int calculatePrice() {
return itemPrice - discountPrice;
}
//생성자
public Order(Long memberId, String itemName, int itemPrice, int discountPrice) {
this.memberId = memberId;
this.itemName = itemName;
this.itemPrice = itemPrice;
this.discountPrice = discountPrice;
}
//게터, 세터
public Long getMemberId() {
return memberId;
}
public void setMemberId(Long memberId) {
this.memberId = memberId;
}
public String getItemName() {
return itemName;
}
public void setItemName(String itemName) {
this.itemName = itemName;
}
public int getItemPrice() {
return itemPrice;
}
public void setItemPrice(int itemPrice) {
this.itemPrice = itemPrice;
}
public int getDiscountPrice() {
return discountPrice;
}
public void setDiscountPrice(int discountPrice) {
this.discountPrice = discountPrice;
}
@Override
public String toString() {
return "Order{" +
"memberId=" + memberId +
", itemName='" + itemName + '\'' +
", itemPrice=" + itemPrice +
", discountPrice=" + discountPrice +
'}';
}
}
🌈 OrderService
public interface OrderService {
Order createOrder(Long memberId, String itemName, int itemPrice);
}
🌈 OrderServiceImpl
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);
}
}
악덕의 기획자가 기존의 할인정책(FixDiscountPolicy)을 변경하고 싶다고 한다‼️ 우리는 객체지향 설계 원칙을 잘 준수했기 때문에 DiscountPolicy 인터페이스를 구현한 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;
} else {
return 0;
}
}
}
할인 정책을 변경하려면 클라이언트인 OrderServiceImpl 코드를 고쳐야 한다.
public class OrderServiceImpl implements OrderService {
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
우리는 역할과 구현을 충실하게 분리했다. OK
다형성도 활용하고, 인터페이스와 구현 객체를 분리했다. OK
OCP, DIP 같은 객체지향 설계 원칙을 충실히 준수했다.
→ 그렇게 보이지만 사실은 아니다‼️
DIP: 주문서비스 클라이언트( OrderServiceImpl )는 DiscountPolicy 인터페이스에 의존하면서 DIP를 지킨 것 같은데?
→ 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고있다‼️
→ 추상(인터페이스) 의존: DiscountPolicy
→ 구체(구현) 클래스: FixDiscountPolicy , RateDiscountPolicy
🔎 지금까지 단순히 DiscountPolicy 인터페이스만 의존한다고 생각했다.
잘보면 클라이언트인 OrderServiceImpl 이 DiscountPolicy 인터페이스 뿐 만 아니라 FixDiscountPolicy 인 구체 클래스도 함께 의존하고 있다. 실제 코 드를 보면 의존하고 있다! DIP 위반
OCP: 변경하지 않고 확장할 수 있다고 했는데!
→ 지금 코드는 기능을 확장해서 변경하면, 클라이언트 코드에 영향을 준다! 따라서 OCP를 위반한다‼️


→ 인터페이스에만 의존하도록 설계와 코드를 변경했다.
→ 그런데 구현체가 없는데 어떻게 코드를 실행할 수 있을까?
→ 실제 실행을 해보면 NPE(null pointer exception)가 발생한다.
이 문제를 해결하려면 누군가가 클라이언트인 OrderServiceImpl 에 DiscountPolicy 의 구현 객체를 대신 생성하고 주입해주어야 한다. → 뒤에 나올 AppConfig 클래스, 스프링 컨테이너의 역할‼️
애플리케이션의 전체 동작 방식을 구성하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 설정 클래스를 만든다.
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(new MemoryMemberRepository(),new FixDiscountPolicy());
}
...
}
⭐️ 구현 객체를 생성한다
⭐️ 생성한 구현 객체 인스턴스의 참조를 생성자를 통해서 주입한다.
🌈 MemberServiceImpl 클래스의 생성자 주입
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
//생성자 주입
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
생성자를 주입하므로서 MemberServiceImpl는 MemoryMemberRepository에 의존하지 않고, 인터페이스 MemberRepository에만 의존하므로 DIP 원칙을 준수한다!
즉, MemberServiceImpl의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부( AppConfig )에서 결정된다. MemberServiceImpl 은 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다.

⭐️ 객체의 생성과 연결은 AppConfig 가 담당한다.
⭐️ DIP 완성: MemberServiceImpl 은 MemberRepository 인 추상에만 의존하면 된다. 이제 구체 클래스를 몰라도 된다.
⭐️ 관심사의 분리: 객체를 생성하고 연결하는 역할과 실행하는 역할이 명확히 분리되었다.
🌈 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;
}
...
}
설계 변경으로 OrderServiceImpl 은 FixDiscountPolicy 를 의존하지 않는다! 단지 DiscountPolicy 인터페이스만 의존한다. OrderServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없다. OrderServiceImpl 의 생성자를 통해서 어떤 구현 객체을 주입할지는 오직 외부( AppConfig )에서 결정한다. OrderServiceImpl 은 이제부터 실행에만 집중하면 된다.
🌈 테스트 코드
class OrderServiceTest {
MemberService memberService;
OrderService orderService;
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
}
}