3장. 스프링 핵심 원리 이해2 – 객체 지향 원리 적용

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가지 책임을 가진다.

  1. 객체 생성
  2. 의존관계 연결
  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장 핵심 요약

  1. 새로운 정책 등장 → OrderServiceImpl 수정 필요 → OCP / DIP 위반 발생
  2. 책임 분리 필요 → “객체 생성/연결”과 “실행”을 분리해야 함
  3. AppConfig 등장 → 모든 객체 생성과 의존관계를 관리
  4. DIP / OCP를 만족하는 완전한 객체 지향 구조 완성
  5. 다음 단계는 “스프링 컨테이너로 옮기기”