스프링/SpringBasicCore

Section2 요구사항에 맞추어 순수 자바로 개발

나는웅쓰 2024. 1. 30. 11:59
인프런 강의 中 
김영한 강사님의 '스프링 핵심 원리' 내용을 정리했습니다.

이번 강의부터 순수 자바를 이용해 개발을 진행한다. 처음에는 OCP, DIP가 지켜지지 않는 코드를 작성해보고, 이후에 코드를 리팩토링하며 좋은 객체지향 설계 원칙을 지켰을 때의 효과를 뚜렸하게 느껴보는 것이 Session2, Session3의 목표이다. 그럼 요구사항에 맞춰서 개발해보자!

 

요구사항과 설계

Store에서 주문 서비스 프로그램을 의뢰했다고 가정한다. 

회원

  • 회원가입, 조회기능이 있다.
  • VIP, BASIC 등급으로 나누어진다.
  • DB를 자체 구축할 수 있고, 외부 시스템과 연동할 수도 있다.(미정)

할인

  • 할인 기능이 있다.
  • 고정 할인 정책 또는 회원 할인 정책 두 가지 중 하나를 사용할 수 있다.(미정)

주문

  • 주문 기능이 있다.
  • 주문시, 주문한 회원을 조회 후 할인을 적용하고 주문 내역을 반환한다.

회원 도메인 설계

회원 도메인 협력 관계

위의 요구사항대로 회원 도메인 협력 관계를 다이어그램으로 표현했다. 나는 개인적으로 협력 관계보다 의존 관계를 표현했다고 이해하고 넘어갔다. 회원 저장소는 아직 어떤 저장소를 사용할 지 모르기 때문에 저장소의 역할(Interface)와 역할을 구현할 구현체(Class)로 나누어 설계를 진행했다. 위 다이어그램을 봐도 어떤 저장소를 사용하는지 모르겠다면 관계를 제대로 표현한 것이다.

 

회원 클래스 다이어그램

클래스 다이어그램을 보면 Service도 역할과 구현체로 잘 분리해서 설계한 모습을 확인할 수 있고, 가장 중요한 건!! 서비스 구현체가 회원 저장소 역할을 하는 MemberRepository만 의존하고 있고 저장소의 구현체에는 의존하지 않도록 설계한 부분이다! 위 설계처럼 코드를 작성하면 OCP, DIP를 위반하지 않을 수 있다!(하지만 우리는 처음에 위반한 코드를 작성해볼 것이다.)

회원 객체 다이어그램

객체 다이어그램을 확인하면 실제로 어떤 구현체를 사용하는지 한 눈에 확인할 수 있다.(클래스 다이어그램과의 차이점)

회원 도메인 개발

회원 등급 엔티티

package hello.core.member;

public enum Grade {
 BASIC,
 VIP
}

enum을 사용해 BASIC, VIP이외에 다른 값은 들어오지 못하게 개발을 진행했다.

 

회원 VO

package hello.core.member;

@Getter @Setter
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;
 	}
}

코드가 너무 길어져서 가독성을 위해 get, set은 Annotation을 사용해 표현했다.

 

회원 저장소(Repository)

package hello.core.member;

public interface MemberRepository {
 	void save(Member member);
 	Member findById(Long memberId);
}

 

메모리 회원 저장소 구현체

package hello.core.member;
import java.util.HashMap;
import java.util.Map;

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

회원 저장소의 구현체이므로 MemberRepository 인터페이스를 상속 받아 Override(재정의)해서 사용한다.

 

회원 서비스

회원 서비스는 회원가입, 조회 기능만 개발하면 된다. 클래스 다이어그램처럼 역할과 구현체를 잘 나누고 저장소에서 저장하거나 조회를 할 수 있게 MemberRepository 인터페이스에 의존하도록 개발해보자.

 

회원 서비스 인터페이스(역할 담당)

package hello.core.member;

public interface MemberService {
 	void join(Member member);
 	Member findMember(Long memberId);
}

 

회원 서비스 구현체

package hello.core.member;

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

위와 같이 서비스를 위해 코드를 작성했다면, 서비스가 private final MemberRepository memberRepository = new MemoryMemberRepository(); 코드를 통해서 저장소에 의존하고 있기 때문에 서비스 호출 시 회원가입(join)과 조회(findMember)를 수행할 수 있다.

 

위처럼 코드를 작성했다면, 회원 서비스는 완성한 것이다. Junit을 통해 테스트해 볼 수 있다.(나는 생략)
아마 테스트를 해보면 코드가 문제없이 잘 돌아가겠지만 위 코드는 처음 다이어그램으로 설계했던 내용과는 조금 다르게 설계가 되었다. 그래서 OCP, DIP를 지키지 못한 코드가 되었다. 어느 부분에서 코드를 잘못 작성했을까? 이번에는 위와 같은 방법처럼 주문, 할인 서비스를 작성해 보고 할인 정책을 변경해 보며 코드의 허점을 찾아보겠다.

 

주문과 할인 도메인 설계

주문 도메인

회원 서비스는 저장소에 의존하며 회원가입 기능과 조회 기능을 사용하면 됐지만 주문 서비스는 할인 정책에 맞는 할인 서비스도 수행해야 하기 때문에 할인 정책을 가지고 있는 인터페이스에도 의존해야 해서 뭔가 되게 복잡해 보인다.(하지만 사실 인터페이스 하나 더 의존한 것이 끝이니 겁먹지 말자.) 주문 서비스 역시 역할과 구현으로 분리해서 설계했기 때문에 구현 객체를 유연하게 변경할 수 있다.

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

 

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

우리는 객체 다이어그램과 같이 메모리 회원 저장소와 정액(고정) 할인 정책을 사용해 주문 서비스를 구현해볼 것이다. 그리고 나중에 할인 정책을 정률 할인 정책으로 변경해보며 DIP, OCP가 왜 위반되고 있는지를 확인해볼 것이다.

 

주문과 할인 도메인 개발

할인 정책 인터페이스

package hello.core.discount;
import hello.core.member.Member;
public interface DiscountPolicy {
 /**
 * @return 할인 대상 금액
 */
 int discount(Member member, int price);
}

 

정액 할인 정책 구현체

package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;

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

등급이 VIP라면 1000원 고정 할인을 받을 수 있도록 설계

 

주문 VO

package hello.core.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;
 }
 
 public int calculatePrice() {
 	return itemPrice - discountPrice;
 }
 
 @Override
 public String toString() {
 	return "Order{" +
 	"memberId=" + memberId +
 	", itemName='" + itemName + '\'' +
 	", itemPrice=" + itemPrice +
 	", discountPrice=" + discountPrice +
 	'}';
 }

할인된 금액을 계산하기 위해 calculatePrice() 메서드를 생성했다. 리턴된 주문 내역을 확인하기 위해 다음과 같이 toString()을 재정의했다.

 

주문 서비스 인터페이스

package hello.core.order;
public interface OrderService {
 Order createOrder(Long memberId, String itemName, int itemPrice);
}

 

주문 서비스 구현체

 

package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;
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);
 }
}

 

주문 할인 정책 실행

package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.order.Order;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class OrderApp {
 public static void main(String[] args) {
 
 	MemberService memberService = new MemberServiceImpl();
 	OrderService orderService = new OrderServiceImpl();
 
	 long memberId = 1L;
 	Member member = new Member(memberId, "Hyeonung", Grade.VIP);
 	memberService.join(member);
 
 	Order order = orderService.createOrder(memberId, "itemA", 10000);
 	System.out.println("order = " + order);
 	}
}

위와 같이 main안에서 서비스 객체 생성 후 코드를 실행하면

다음과 같은 결과가 나온다. 입력한 memberId, itemName, itemPrice가 잘 반환되고, 등급을 VIP로 입력했기 때문에 고정된 할인값 1000원이 반환된 것을 확인할 수 있다.

 

김영한 강사님의 강의대로 우리는 코드 작성을 마치고 결과물을 원하는 데로 잘 출력했다. 원하는 결과물도 나왔으니 이대로만 잘 작성하면 되지 않는가? 라는 생각이 없어질 수 있도록 다음 시간에 할인 정책을 변경해보며 구현체 변경시 위 코드는 어떤 문제점이 있는지 살펴보자!!

 

!!김영한 강사님 강의

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8