목적
“스프링 없이 객체 지향 원칙(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. 역할과 구현을 분리한 구조
역학(인터페이스)
MemberRepositoryMemberService
구현체(Class)
MemoryMemberRepositoryMemberServiceImpl
구조 그림으로 표현하면 이렇게 된다.
[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;
}
}
여기까지 보면 세 가지 사실을 깨달을 수 있다.
- 역할과 구현을 분리했다.
- 다형성을 활용할 수 있는 구조다.
- 그러나 아직 문제가 있다.
그 문제는 3장에서 등장한다.
5. 실행과 테스트
마지막으로 순수 자바 환경에서 테스트 클래스를 만들어 실행해볼 수 있다.
예)
MemberService memberService = new MemberServiceImpl(new MemoryMemberRepository());
하지만 이 방식은 다음과 같은 치명적인 문제가 있다.
클라이언트 코드가 구현체를 직접 선택해야 하는 구조
→ DIP 위반
→ OCP 위반
이 문제를 해결하기 위해 3장에서 AppConfig가 등장한다.
2장 핵심 요약
- 2장은 “스프링 없이 객체 지향 설계를 직접 해보는 장”이다.
- 역할(인터페이스)과 구현(구체 클래스)을 분리하는 것이 가장 중요한 흐름.
- 하지만 아직 DIP, OCP 문제를 완전히 해결하지 못한 상태다.
- 이 문제를 해결하기 위해 3장에서 스프링 스타일의 구성 클래스(AppConfig)가 등장한다.