[Spring] Basic, 스프링 핵심 원리 이해 ①

홍정완·2022년 8월 5일
0

Spring

목록 보기
12/32
post-thumbnail

스프링의 핵심 원리 이해를 위해 순수 자바로 예제를 만들어보자 ❗



비즈니스 요구사항과 설계


회원

  • 회원 가입, 조회 기능 ⭕
  • 회원 등급은 일반, VIP 두 가지 등급
  • 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)

주문과 할인 정책

  • 회원, 상품 주문 ⭕

  • 모든 VIP는 1000원을 할인해 주는 고정 금액 할인 적용 (변경 가능)

    • 할인 정책 요구사항을 보면 회원 데이터나 할인 정책 등은 확정한 게 없다.
    • 결정이 될 때까지 객체 지향 설계를 통해 개발 시작


회원 도메인 설계


회원 도메인 협력 관계


  • 회원 가입, 조회 기능 ⭕

  • 회원 등급은 일반, VIP 두 가지 등급

  • 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)

    • 회원 데이터를 조회한다는 책임(역할)을 다하기 위해서 구현하는 방식은 DB,
      기타 외부 시스템 연동 등 다양하다.
    • 미확정이기에 우선 개발을 위해 간단한 메모리 저장소로 역할을 구현



회원 클래스 다이어그램


  • MemberService 👉 interface
  • MemberServiceImpl 👉 역할 구현
  • 회원 저장소 역할은 미확정이고 아직 구현할 수 없기에 테스트를 위한
    MemoryMemberRepository에서 구현



회원 객체 다이어그램


  • 실제 서버에 올라가면 객체 간의 참조가 어떻게 되는지를 다이어그램으로 표현


회원 도메인 개발


// 회원 등급
public enum Grade {

 	BASIC,
 	VIP
}
// 회원 엔티티
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 interface MemberRepository {

 	void save(Member member);
    
 	Member findById(Long memberId);
}
// 메모리 회원 저장소 구현체
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);
 	}
    
}

✅ HashMap은 동시성 이슈가 발생할 수 있다. 이런 경우 ConcurrentHashMap 사용



// 회원 서비스 인터페이스
public interface MemberService {

 	void join(Member member);
    
 	Member findMember(Long memberId);
}
// 회원 서비스 구현체
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);
 	}
    
}

회원 도메인 설계의 문제점

  • 의존관계가 인터페이스뿐만 아니라 구현까지 모두 의존하는 문제가 있다.


주문과 할인 도메인 설계


  • 주문과 할인 정책
    • 회원은 상품 주문 ⭕
    • 회원 등급에 따라 할인 정책 적용
    • 할인 정책은 변경 가능성이 높다. (미확정)

주문 도메인 협력, 역할, 책임


  • 주문 생성 : 클라이언트는 주문 서비스에 주문 생성 요청
  • 회원 조회 : 할인을 위해서는 회원 등급이 필요, 주문 서비스는 회원 저장소에서 회원 조회
  • 할인 적용 : 주문 서비스는 회원 등급에 따른 할인 여부를 할인 정책에 위임
  • 주문 결과 반환 : 주문 서비스는 할인 결과를 포함한 주문 결과 반환

주문 도메인 전체


저장소와 정책의 구현체를 통해 확장 및 변경이 용이해졌다.
이렇게 설계가 됨으로써 회원 저장소 역할에서 회원 저장소를 메모리, DB, 외부 API 등 자유롭게 변경이 가능해졌고, 미확정된 정책들에 대해서 유연하게 대처가 가능하다.


주문 도메인 클래스 다이어그램


주문 도메인 객체 다이어그램 ①


회원을 메모리에서 조회하고, 정액 할인 정책(고정 금액)을 지원해도 주문 서비스를 변경하지 않아도 된다. 역할들의 협력 관계를 그대로 재사용 ⭕


주문 도메인 객체 다이어그램 ②


회원을 메모리가 아닌 실제 DB에서 조회하고, 정률 할인 정책(주문 금액에 따라 % 할인)을 지원해도 주문 서비스를 변경하지 않아도 된다. 협력 관계를 그대로 재사용 ⭕



주문과 할인 도메인 개발


// 할인 정책 인터페이스
public interface DiscountPolicy {
 
 	int discount(Member member, int price);
}
// 정액 할인 정책 구현체
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;
 		}
 	}
}
// 주문 엔티티
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;
	}
    
	public int calculatePrice() {
 		return itemPrice - discountPrice;
 	}
    
	// get, set ...
}
// 주문 서비스 인터페이스
public interface OrderService {
 	Order createOrder(Long memberId, String itemName, int itemPrice);
}
// 주문 서비스 구현체
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);
	 }
}

주문 생성 요청이 오면, 회원 정보를 조회하고, 할인 정책을 적용한 다음 주문 객체를 생성해서 반환한다. 메모리 회원 리포지토리와, 고정 금액 할인 정책을 구현체로 생성한다.




스프링 없이 예제 코드 작성을 통해 회원-주문 애플리케이션을 요구사항 분석부터 작성하면서 구현을 해 보았다. 그리고 그 과정에서 아직 OCP, DIP 원칙을 지킬 수 없다는 점을 확인했다.
다음 포스팅에서는 이런 문제들을 인지하고 객체지향 원리를 어떻게 적용하는지 살펴보자

profile
습관이 전부다.

0개의 댓글