2장. 스프링 핵심 원리 이해1 – 예제 만들기

목적

“스프링 없이 객체 지향 원칙(SOLID)을 적용해 회원 · 주문 · 할인 시스템을 구축해보는 것”

여기서 만든 구조는 3장에서 스프링으로 자연스럽게 변환된다.


1. 프로젝트 시작: 순수 자바 환경 만들기

  • Java 17 이상
  • Build tool: Gradle
  • Dependencies 없음 (즉, 스프링 없이 시작)

왜 스프링 없이 시작할까?

  • 객체 지향 설계 자체를 먼저 이해해야
  • 스프링의 역할이 명확하게 보이기 때문

스프링을 먼저 배우면 “왜 필요한지”를 모른 채 따라 하기만 하게 된다.


2. 비즈니스 요구사항 분석

이제 실제 예제를 만들기 위해 요구사항을 정리한다.

회원(Member)

  • 회원을 가입하고 조회할 수 있어야 한다.
  • 회원 등급은 2종류
    • BASIC
    • VIP
  • 회원 저장 방식은 미확정
    • 자체 DB를 쓸 수도 있음
    • 외부 시스템과 연동될 수도 있음 → 즉, 구현은 바뀔 수 있음

주문(Order) + 할인(Discount)

  • 회원은 상품을 주문한다.
  • VIP는 할인 혜택을 받는다.
  • 회사에서는 아직 할인 정책을 고정하지 못한 상태
    • 현재는 “VIP → 1000원 고정 할인”
    • 나중에는 정책 변경 가능

여기서 가장 중요한 힌트가 나온다.

“정책은 바뀔 수 있다.”

정책이 바뀌면 클라이언트(Service)의 코드까지 바뀐다면 OCP, DIP 위반이 되기 때문에

인터페이스로 역할을 분리해서 유연한 구조를 만들어야 한다.


3. 회원 도메인 설계

3-1. 역할과 구현을 분리한 구조

역학(인터페이스)

  • MemberRepository
  • MemberService

구현체(Class)

  • MemoryMemberRepository
  • MemberServiceImpl

구조 그림으로 표현하면 이렇게 된다.

[MemberService] <─사용─ [MemberServiceImpl]
       ▲                            │
       │                            ▼
[MemberRepository] <─사용─ [MemoryMemberRepository]

핵심은 MemberServiceImpl이 구현체를 직접 new 하지 않고

인터페이스만 의존하도록 설계해야 한다는 것.

실제 코드 설계 개요

Member 엔티티

public class Member {
    private Long id;
    private String name;
    private Grade grade;
}

Grade enum

public enum Grade {
    BASIC, VIP
}

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<>();

    public void save(Member member) {
        store.put(member.getId(), member);
    }

    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;

    // 생성자 주입
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    public void join(Member member) {
        memberRepository.save(member);
    }

    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

여기서 중요한 점 하나!

MemberServiceImpl은 MemoryMemberRepository를 직접 new 하지 않는다.

이유는?

  • 나중에 DB가 바뀔 수도 있고
  • Redis로 바뀔 수도 있고
  • 외부 API로 바뀔 수도 있기 때문

외부에서 어떤 구현을 넣을지 통제하도록 하는 것이 핵심.


4. 주문(Order) + 할인(Discount) 설계

주문 생성 로직 → “회원 정보 조회 + 할인 정책 적용 + 최종 주문 만들기”

구조는 다음과 같다.

[OrderService] <─사용─ [OrderServiceImpl]
       ▲                            │
       │                            ▼
[MemberRepository]           [DiscountPolicy]
                             ▲          ▲
                             │          │
                [FixDiscountPolicy]  [RateDiscountPolicy]

초기 정책은 “VIP → 1000원 할인”

이 정책은 FixDiscountPolicy로 구현한다.

나중에 10% 할인 정책이 생기면

RateDiscountPolicy를 추가로 구현하면 된다.

DiscountPolicy (인터페이스)

public interface DiscountPolicy {
    int discount(Member member, int price);
}

FixDiscountPolicy

public class FixDiscountPolicy implements DiscountPolicy {
    private int discountFixAmount = 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;
    }
}

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;
    }
}

여기까지 보면 세 가지 사실을 깨달을 수 있다.

  1. 역할과 구현을 분리했다.
  2. 다형성을 활용할 수 있는 구조다.
  3. 그러나 아직 문제가 있다.

그 문제는 3장에서 등장한다.


5. 실행과 테스트

마지막으로 순수 자바 환경에서 테스트 클래스를 만들어 실행해볼 수 있다.

예)

MemberService memberService = new MemberServiceImpl(new MemoryMemberRepository());

하지만 이 방식은 다음과 같은 치명적인 문제가 있다.

클라이언트 코드가 구현체를 직접 선택해야 하는 구조

→ DIP 위반

→ OCP 위반

이 문제를 해결하기 위해 3장에서 AppConfig가 등장한다.


2장 핵심 요약

  • 2장은 “스프링 없이 객체 지향 설계를 직접 해보는 장”이다.
  • 역할(인터페이스)과 구현(구체 클래스)을 분리하는 것이 가장 중요한 흐름.
  • 하지만 아직 DIP, OCP 문제를 완전히 해결하지 못한 상태다.
  • 이 문제를 해결하기 위해 3장에서 스프링 스타일의 구성 클래스(AppConfig)가 등장한다.