김영한씨의 스프링 핵심 원리 - 기본편 강의를 듣고 공부 겸 정리하는 글입니다.
앞으로 다룰 예제를 만드는데 집중하는 포스팅이 될 것 같아요.
위의 요구사항을 보면 DB를 선택하는 것이나 할인 정책 같은 경우, 확실하지 않습니다.
그렇다면 개발을 계속 미루어야 하는가,,?
객체 지향 설계 방법을 이용해서 추후에 바뀌어도 쉽게 바꿀 수 있게 개발을 하는것이 목표!!
인터페이스를 만들고, 추후에 정해지면 구현체를 만들어 갈아끼우면 됨!
클라이언트 -> 회원 서비스(가입, 조회) -> 회원 저장소
메모리 회원 저장소 ---> 회원 저장소
DB 회원 저장소 ---> 회원 저장소
외부 시스템 연동 회원 저장소 ---> 회원 저장소
MemberServiceImpl ---> MemberService
MemberServiceImpl -> MemberRepository
MemoryMemberRepository ---> MemberRepository
DbMemberRepository ---> MemberRepository
클라이언트 -> 회원 서비스(MemberServiceImpl) -> 회원 저장소
member
member 패키지 아래에서 파일을 생성합니다.
public enum Grade {
BASIC,
VIP
}
enum으로 만들었기 때문에 모두 대문자로 작성합니다.
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 String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Grade getGrade() {
return grade;
}
public void setGrade(Grade grade) {
this.grade = grade;
}
}
회원 아이디와, 이름, 등급을 필드로 넣어주고 생성자와 getter, setter를 만들어 줍니다.
public interface MemberRepository {
void save(Member member);
Member findById(Long memberId);
}
데이터 베이스 내에서 회원 가입, 조회하는 interface를 담당합니다.
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);
}
}
어떤 저장소를 이용할지 정해지지 않았지만, 우선 메모리 회원 저장소를 구현해서 이를 이용하겠습니다.
Interface를 상속하기 위해 implements를 이용합니다.
public interface MemberService {
void join(Member member);
Member findMember(Long memberId);
}
회원 서비스와 관련해서 가입, 조회를 가진 interface입니다.
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 class MemberApp {
public static void main(String[] args) {
MemberService memberService = new MemberServiceImpl();
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
Member findMember = memberService.findMember(member.getId());
System.out.println("member = " + member);
System.out.println("findMember = " + findMember);
}
}
어플리케이션 로직으로 이런식으로 테스트 하는 방법은 옳지 않습니다.
테스트 파일을 따로 만들어 거기에서 테스트하도록 해요.
public class MemberServiceTest {
MemberService memberService = new MemberServiceImpl();
@Test
void join() {
// given
Member member = new Member(1L, "memberA", Grade.VIP);
// when
memberService.join(member);
Member findMember = memberService.findMember(member.getId());
// then
Assertions.assertThat(member).isEqualTo(findMember);
}
}
테스트 코드를 작성할 때에는 @Test 어노테이션을 붙여줍니다.
Junit에서 제공하는 Assertions을 이용했는데 단정문이라고 하네요. 성공하지 않는다면 테스트는 실패입니다.
즉, member와 findMember가 isEqualTo(객체가 같아야 함)해야 테스트가 성공하게 됩니다.
memberService의 findMember를 이용해서 찾았으니 이 둘은 같아야겠죠?
문제점은 무엇일까요?!
저장소를 바꾸게 된다면, OCP의 원칙을 준수하지 못합니다. 왜냐하면 OCP는 변경에 있어 닫혀있어야 하는데 구현체를 직접 변경하게 되니까요!
또한, 의존관계를 보면 인터페이스 뿐만 아니라 구현체에도 의존하게 된다는 걸 알 수 있어요.
예시) MemberService(인터페이스)뿐만 아니라 MemberServiceImpl(구현체)
MemberService memberService = new MemberServiceImpl();
이렇게 구현체만 갈아끼우고 협력 관계를 그대로 사용할 수 있다!
다형성의 힘~!
order, discount
order과 discount 패키지 아래에서 파일을 생성합니다.
public interface DiscountPolicy {
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;
}
}
}
회원 등급이 VIP라면, 1000원을 할인해줍니다.
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 Long getMemberId() {
return memberId;
}
public void setMemberId(Long memberId) {
this.memberId = memberId;
}
public String getItemName() {
return itemName;
}
public void setItemName(String itemName) {
this.itemName = itemName;
}
public int getItemPrice() {
return itemPrice;
}
public void setItemPrice(int itemPrice) {
this.itemPrice = itemPrice;
}
public int getDiscountPrice() {
return discountPrice;
}
public void setDiscountPrice(int discountPrice) {
this.discountPrice = discountPrice;
}
}
주문자의 id, 아이템 이름, 가격, 할인 가격을 필드로 갖고, 생성자, getter와 setter를 갖습니다.
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);
}
}
주문 비즈니스 로직을 담당하고, 메모리 회원 리포지토리와, 고정 금액 할인 정책을 구현체로 생성합니다.
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, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "memberA", 10000);
System.out.println("order = " + order);
}
}
테스트 파일을 따로 만들어서 테스트하도록 해요.
public class OrderServiceTest {
MemberService memberService = new MemberServiceImpl();
OrderService orderService = new OrderServiceImpl();
@Test
void createOrder() {
long memberId = 1L;
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 10000);
Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
}
}
회원 등급이 VIP일 때, 할인 가격이 1000원이 맞으면 테스트를 통과한답니다. memberA는 Grade.VIP가 맞기 때문에 테스트에 통과하지요,,
이번에는 앞으로 다룰 예제를 만드는데 집중했지만, 다음번엔 객체 지향적으로 다시 설계하는 포스팅을 가져오겠습니다!