본문 바로가기
스터디IT🌼/SpringBoot

[ SpringBoot ] 스프링 핵심 원리 기본편_핵심원리 이해 (with. 예제)

by 동백사과 2022. 10. 13.

** 본 글은 우아한 형제들 김영한 개발자님 강의를 기반으로 직접 재구성한 글 입니다. **

 

🎯 비즈니스 요구 사항과 설계

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

 

  • 주문과 할인 정책
    회원은 상품을 주문할 수 있다.
    회원 등급에 따라 할인 정책을 적용할 수 있다.
    할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수 있다.)
    할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. 최악의 경우 할인을 적용하지 않을 수 도 있다. (미확정)

  💡 미확정인 부분이 있지만 객체 지향 설계 방법을 통해 결정 전에도 개발 가능!  ( 인터페이스를 만들고 구현체를 언제든지 바꿔 낄 수 있도록 설계 )

 

🎯 도메인 설계 & 개발

 1. 회원 도메인 설계

회원 도메인 설계

 

회원 클래스 다이어그램

- 회원 객체 다이어그램은 실제 실행시 사용되는 객체를 의미한다. 본 회원 객체 다이어그램에 있는 회원 서비스는 MemberServiceImpl을 의미한다. 

- 회원 클래스 다이어그램만으로는 실제 실행 시 어떤 클래스가 이용되는지 명확히 알 수 없어 회원 객체 다이어그램을 두어 이를 확인한다. 

- 클래스 다이어그램은 정적, 객체 다이어그램은 동적인 것으로 볼 수 있음

 

2. 회원 도메인 개발

package hello.hellospring.member;

# 회원 등급 표기를 위한 enum 클래스
public enum Grade {

    BASIC,
    VIP
}
package hello.hellospring.member;

# 회원 객체 클래스 생성
# cmd + n : constructor, 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;
    }

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

 

회원 저장소에 해당하는 인터페이스

package hello.hellospring.member;

public interface MemberRepository {

    void save(Member member);

    Member findById(Long memberId);
}

 

회원 저장소 구현체에 해당하는 클래스 - MemoryMemberRepository

package hello.hellospring.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);
    }
}

 

회원 서비스 인터페이스

package hello.hellospring.member;

public interface MemberService {

    void join(Member member); // 회원가입
    
    Member findMember(Long memberId); // 회원 조회
    
}

 

회원 서비스 구현체 

package hello.hellospring.member;

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

 

3. 회원 도메인 실행과 테스트

3-1. 1번 예제는 순수 java 만을 이용하여 구현한 기본 예시  ( 비효율적 )

package hello.hellospring;

import hello.hellospring.member.Grade;
import hello.hellospring.member.Member;
import hello.hellospring.member.MemberService;
import hello.hellospring.member.MemberServiceImpl;

public class MemberApp {

    public static void main(String[] args) { // psvm 치면 자동완성
        MemberService memberService = new MemberServiceImpl();

        Member member = new Member(1L, "memberA", Grade.VIP); // cmt+opt+ v : 자동완성

        memberService.join(member);

        Member findMember = memberService.findMember(1L);
        System.out.println("findMember = " + findMember.getName()); //soutv 치면 자동완성


    }
}

 

3-2. Spring의 junit을 이용한 테스트 코드

- 상위 test/hellospring 폴더 아래 member package를 만들고 아래 테스트 코드 파일 생성

- 상위 java 아래 있던 동일한 구조를 test 아래에 똑같이 만들어 줬다고 보면 됨.

package hello.hellospring.member;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

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(1L);

        //then : 검증
        Assertions.assertThat(member).isEqualTo(findMember); // 회원가입한 멤버와 찾는 멤버가 동일하면 ok

    }
}

- Test 코드임을 명시하는 어노테이션 @Test를 써줘야한다. ( junit.jupiter.api.Test )

- given -> when -> then 구조로 테스트 코드를 로직화하는 것이 좋다

- 비교 및 검증을 위해 Assertions 라이브러리를 사용한다. ( assertj.core.api.Assertions )

 

 

❓ 위 회원 도메인 설계의 문제점 

 Q. 다른 저장소로 변경할 때 OCP의 원칙을 잘 준수하는가?

 A. 잘 준수하지 못한다. 새로운 저장소로 변경할 경우 new 해서 저장소 객체를 넣어주었던 부분 코드들을 수정해야하는 문제가 발생한다. 

 

 Q. DIP를 잘 지키고 있는가?

 A. 추상화에 의존해야지, 구체화에 의존하면 안된다 라는 DIP원칙에 위배되는 부분이 있다. 의존관계가 인터페이스 뿐 아니라 구현까지 모두 의존하는 문제가 있다. 위 예로 MemberServiceImpl은 MemberService 인터페이스에도 의존하고 MemoryMemberRepository 구현체에도 의존한다. 

 

 

4. 주문과 할인 도메인 설계

주문과 할인 도메인 설계

주문과 할인 정책

  • 회원은 상품을 주문할 수 있다.
  • 회원 등급에 따라 할인 정책을 적용할 수 있다.
  • 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수 있다.)
  • 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. 최악의 경우 할인을 적용하지 않을 수 도 있다. (미확정) 
주문과 할인 도메인 설계
  • 할인 정책의 경우 변동가능성이 높아 인터페이스와 구현체를 분리해서 구현한다!!!!
  • 데이터베이스, 할인 정책 등이 추후 변경 되어도 주문 서비스를 변경하지 않아도 된다.
  • 회원 저장소 뿐 아니라 할인 정책까지 유연하게 변경할 수 있는 구조!

클래스 다이어그램

 

5. 주문과 할인 도메인 개발 

 

할인 정책 인터페이스

package hello.hellospring.discount;

import hello.hellospring.member.Member;

public interface DiscountPolicy {

    /**
     *
     * @param member
     * @param price
     * @return 할인 대상 금액
     */
    int discount(Member member, int price); // f2 누르면 오류 난 곳으로 이동
}

 

할인 정책 구현체

package hello.hellospring.discount;

import hello.hellospring.member.Grade;
import hello.hellospring.member.Member;

public class FixDiscountPolicy implements DiscountPolicy{
    private int discountFixAmount = 1000;

    @Override
    public int discount(Member member, int price) {
        if (member.getGrade() == Grade.VIP){ //멤버 등급이 vip 일 때
            return discountFixAmount;
        }else{
            return 0;
        }
    }
}

 

주문 객체 생성

package hello.hellospring.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;
    }

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

    @Override
    public String toString() {
        return "Order{" +
                "memberId=" + memberId +
                ", itemName='" + itemName + '\'' +
                ", itemPrice=" + itemPrice +
                ", discountPrice=" + discountPrice +
                '}';
    }
}

 

주문 서비스 인터페이스

package hello.hellospring.order;

public interface OrderService {
    Order createOrder(Long memberId, String itemName, int itemPrice );
}

 

주문 서비스 구현체

package hello.hellospring.order;

import hello.hellospring.discount.DiscountPolicy;
import hello.hellospring.discount.FixDiscountPolicy;
import hello.hellospring.member.Member;
import hello.hellospring.member.MemberRepository;
import hello.hellospring.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); //cmd+opt+v
        // 할인 금액 알아오기 ( 할인정책 서비스가 처리해줌, order서비스는 그냥 받아서 쓰면됨)
        int discountPrice = discountPolicy.discount(member, itemPrice); // 할인은 할인 정책에서 알아서 처리 , 주문 서비스는 관여 x
        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}

 

6. 주문과 할인 도메인 실행과 테스트

6-1. 1번 예제는 순수 java 만을 이용하여 구현한 기본 예시  ( 비효율적 )

package hello.hellospring;

import hello.hellospring.member.Grade;
import hello.hellospring.member.Member;
import hello.hellospring.member.MemberService;
import hello.hellospring.member.MemberServiceImpl;
import hello.hellospring.order.Order;
import hello.hellospring.order.OrderService;
import hello.hellospring.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, "memberA", Grade.VIP);
        memberService.join(member);
        
        Order order = orderService.createOrder(memberId, "itemA", 10000);
        System.out.println("order = " + order.toString());
    }
}

 

6-2. Spring의 junit을 이용한 테스트 코드

package hello.hellospring.order;

import hello.hellospring.member.Grade;
import hello.hellospring.member.Member;
import hello.hellospring.member.MemberService;
import hello.hellospring.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

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

    }

}

 

🎯 최종 정리

- 다형성을 잘 활용하여 예시를 만들었지만 이후 할인 정책 또는 저장소가 변경 되었을 때도 잘 동작하는가? 에 대한 고민 필요!!

- 인터페이스와 구현을 좀 더 분리할 수 있는 방안을 고려할 필요가 있음

- 객체 지향적 설계 구조에 대해 학습할 수 있었음!!

 

** 다음 글에서 위 문제점을 개선하는 내용을 담을 예정입니다 **


 

댓글