이번 포스트에선 [Spring 기본] 섹션에서 쭉 사용할 도메인 예제들을 알아보고, 지난시간에 살펴보았던 좋은
객체지향의 관점에서 프로그래밍을 해볼 수 있도록 하겠습니다. 지난 포스트에 이어서, [역할과 구현의 분리]가 어떤식으로 이루어지는지를 중점으로 살펴보시면 될 것 같습니다!

필요 비즈니스 로직 생성
1. Domain
(1) Member - 이름, 등급 (VIP, Normal)
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;
}
...
}
(2) Order - 구입 회원, 제품 이름, 제품 가격, 할인 가격
public class Order {
private Long memberId;
private String itemName;
private int itemPrice;
private int discountPrice;
public Order(Long memberId, String itemName, int itemPrice, int discountPrice) {
this.memberId = memberId;
this.itemName = itemName;
this.itemPrice = itemPrice;
this.discountPrice = discountPrice;
}
...
}
(3) DiscountPolicy - 정가 할인, 비율 할인
어떤 할인 정책이 들어올지 미정인 서비스라고 가정하기 때문에, 추후 어떤 할인이든지 활용할 수 있도록
Discount Policy 인터페이스를 만들고, 각 할인 정책을 생성해 놓았습니다. VIP 등급의 고객들에 한해서 할인
이 들어가고, 일반 고객들은 할인 가격이 0입니다. 참고로 인터페이스를 만들 경우 항상 내부 핵심 로직을 설
명해두는 습관을 갖는 것이 좋습니다 (개인적인 경험).
public interface DiscountPolicy {
// 핵심로직을 항상 명시해 두고, 항상 제일 간단할 수 있는 구조
// discount : 할인되는 금액을 알려준다
int discount(Member member, int price);
}
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;
}
}
}
public class RateDiscountPolicy implements DiscountPolicy { // 비율 할인
private int discountRate = 10;
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return price * discountRate / 100;
} else {
return 0;
}
}
}
해당 할인을 적용하고, DB에 저장한다는 시뮬레이션을 돌리기 위해, 다음과 같이 각 Service 와 Repository 역
시 구성해 두었습니다. Spring 을 처음 접하시는 분들을 위해, Repository 는 DB와 직접적으로 통신을 하는 영
역이며, Service 는 이 Repository 를 사용하는 수행 함수들의 영역입니다.
2. Repository
DB 통신을 하는 수단에 따라서 Repository 를 분리하는 설계가 자주 이루어지기 때문에, Repository도
interface 로 구성하는 것을 추천합니다.
public interface MemberRepository {
// Member 를 저장하는 역할 수행
void save(Member member);
// 해당 id 의 Member를 찾아서 반환하는 역할 수행
Member findById(Long memberId);
}
public class MemoryMemberRepository implements MemberRepository { // 임시 기억 장치, 개발/테스트에 사용
private static Map<Long, Member> store = new HashMap<>(); // 간이 DB
@Override
public void save(Member member) {
store.put(member.getId(), member);
}
@Override
public Member findById(Long memberId) {
return store.get(memberId);
}
}
3. Service
Service 영역은 Repository 를 실질적으로 사용하기 위한 계층입니다.
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository = new MemoryMemberRepository();
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
public MemberRepository getMemberRepository(){
return this.memberRepository;
}
}
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
@Override
public Order createdOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
객체지향 5대 원칙 SOLID에 대해서 잘 이해하신 분이라면, 위 비즈니스 로직이 잘못 설계된 것을 바로 아실
수 있으실 겁니다. 다음과 같은 그림을 살펴보겠습니다.
일반적으로 SW 영역에서 '의존한다' 라고 표현을 하게되면, 그냥 단순하게 대상을 '사용한다'라고 인지하시면
됩니다. 즉, Service Layer 코드들을 보시면 아시겠지만, MemberRepository, DiscountPolicy 의 역할을 수행할
객체가 필요한 것을 알 수 있습니다. 하지만, DIP에 따르면 클라이언트는 역할체에만 의존해야 하지, 그 역할
의 구현체까지 의존하면 안됩니다. 하지만 위 코드는 OrderService 클라이언트는 역할의 구현체들인
MemoryMemberRepository(), FixDiscountPolicy() 에도 의존하고 있습니다. 따라서 다음과 같이 바꿔줘야 합
니다.
역할과 구현의 분리
조금 더 직관적으로 이해하기 위해, 지난 포스트에서 생각했던 로미오와 줄리엣 공연을 생각해봅시다. 로미오
라는 역할 (서비스 인터페이스)를 구현하기 위해서는 A씨 (OrderServiceImpl) 가 있습니다. 줄리엣 이라는 역할 (할인 정책 인터페이스)를 구현하기 위해서 역시 C양(정가할인)과 D양(비율할인)이 있습니다. 만약 위에 잘못된 설계처럼 OrderServiceImpl 라는 구현 객체가 직접 Fix Discount Policy에 의존하는, 즉 A씨가 직접 C양을 캐스팅하는 이상한 현상과 같습니다.
그렇다면 보통 연극에서는 누가 캐스팅을 하나요? 당연히 공연 담당자 (기획자)가 진행을 합니다. 그렇다면 이 DIP, OCP, 어떻게 보면 SRP 도 위반하는 문제를 해결하기 위해서는, 우리도 기획자를 만들어 보겠습니다.
기획자를 만들어 구현의 책임을 확실히 분리해봅시다.
AppConfig의 등장
Spring을 하면서 Configuration 을 해주는 클래스는 많이 등장하게 됩니다. 해당 클래스들은 말그대로 우리가
만들려는 Spring App의 설정을 해주는 공간이라고 보면 되고, 우리도 설정 클래스를 만들어서 특정 역할에 어
떤 구현 객체들을 사용할 것인지 지정해줄 것입니다. 지정을 해주기 위해서, 각 구현 클래스에도 생성자들을
통해서 지정될 수 있도록 기본 생성자들을 추가해줬습니다.
public class AppConfig {
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
public DiscountPolicy discountPolicy(){
return new FixDiscountPolicy();
}
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
}
AppConfig Class 를 사용한다면, ServiceImpl 코드들이 다음과 같이 각 구현 객체들의 존재를 모르도록, 의존
하지 않도록 수정해줄 수 있는 점을 확인할 수 있습니다. (철저하게 DIP를 지키고 있는 모습을 확인할 수 있습
니다)
public class MemberServiceImpl implements MemberService{
//private final MemberRepository memberRepository = new MemoryMemberRepository();
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
}
public class OrderServiceImpl implements OrderService {
//private final MemberRepository memberRepository = new MemoryMemberRepository();
//private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy){
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
주입하다
위와 같은 모습을 구현 객체를 주입한다라고 표현 합니다. 위와 같은 방식을 생성자 주입이라고 하죠. App Config 설정 클래스의 등장으로 다이어그램에는 다음과 같은 변동이 생겼습니다.
클라이언트인 Service Impol 입장에서 보면 MemberRepository, DiscountPolicy 에 대한 역할만 가지고 있으
나, AppConfig (외부)에서 구현 객체를 주입해서 클라이언트의 로직을 수행하는데 아무 문제 없이 만들어줍니
다. 이 모습을 마치 외부에서 주입해주는 것 같다고 해서 Dependency Injection (의존관계 주입, 의존성 주
입) 이라고 합니다.
public class OrderServiceTest {
// MemberService memberService = new MemberServiceImpl();
// OrderService orderService = new OrderServiceImpl();
MemberService memberService;
OrderService orderService;
@BeforeEach
void be(){
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
}
위와 같이 AppConfig를 이용해 역할들 설정을 해주면, 각 역할들의 핵심 로직들을 AppConfig에서 설정해준
역할 구현체들의 로직으로 수행되는 모습을 확인할 수 있습니다.
중요한 점이 있습니다. Spring 에 대한 포스트이지만, 우린 이번 포스트에서 Spring 을 단 한번도 사용하지 않
았습니다. 오직 순수한 Java의 성질만을 이용하여 객체 지향 설계를 해볼 수 있었습니다. 다음부터는 이 개념
을 바탕으로 Spring이 어떻게 예술적으로 객체 지향 설계를 도와주는지 살펴보겠습니다.
포스트 요약
- 앱 설계, 특히 인터페이스 구성시 SOLID를 항상 생각하며 설계해야 한다
- 일반적인 Java 만을 가지고는 완벽한 객체지향 설계가 어렵기 때문에, 외부 Configuration 등을 이용한다.
출처
[스프링 기본]으로 엮인 모든 포스트들은 교육 사이트 인프런의 지식공유자이신 김영한님의 [스프링 핵심 원리] 강의를 기반으로 작성되었습니다. 열심히 정리하고 스스로 공부하기 위해 만든 포스트이지만, 제대로 공부하고 싶으시면 해당 강의를 꼭 들으시는 것을 추천드립니다.
스프링 핵심 원리 - 기본편 - 인프런 | 강의
스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...
www.inflearn.com
'Spring > Spring 기본' 카테고리의 다른 글
[Spring 기본] Component Scan을 통한 Bean 자동화 관리 (0) | 2022.10.17 |
---|---|
[Spring 기본] Spring Container의 Singleton 전략 (0) | 2022.10.14 |
[Spring 기본] Container와 Bean과 조금 더 친해져보자 (0) | 2022.10.13 |
[Spring 기본] Spring의 객체 지향 IoC, DI, Bean, Container (0) | 2022.10.13 |
[Spring 기본] Spring을 시작하며 (0) | 2022.10.12 |