관리 메뉴

나구리의 개발공부기록

예제 만들기(순수 Java), 프로젝트 생성, 비즈니스 요구사항과 설계, 회원 도메인 설계 및 개발, 주문과 할인 도메인 설계 및 개발, 도메인 실행과 테스트 본문

인프런 - 스프링 완전정복 코스 로드맵/스프링 핵심원리 - 기본편

예제 만들기(순수 Java), 프로젝트 생성, 비즈니스 요구사항과 설계, 회원 도메인 설계 및 개발, 주문과 할인 도메인 설계 및 개발, 도메인 실행과 테스트

소소한나구리 2024. 1. 26. 16:05

  출처 : 인프런 - 스프링 핵심원리 - 기본편(유료) / 김영한님  
  유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용  

https://inf.run/kCYMv


** 참고

  • 이번 파트는 스프링의 도움없이 순수하게 Java로만 진행하고 다음 파트에서 스프링을 활용해서 문제점을 해결
  • 프로젝트 생성만 스프링부트로 생성

1. 프로젝트 생성

1) spring initializr 활용

(1) Project 생성 정보

  • IDE - IntelliJ
  • Project : Gradle - Groovy Project
  • Spring Boot: 3.x.x -> 정식 버전 중 가장 최신 버전
  • Language: Java
  • Packaging: Jar
  • Java: 21 or 17
  • Project Metadata
    • group: hello
    • artifact: core
    • Dependencies: 선택X(순수 자바로 진행)
  • IntelliJ 설정
    • Build and run using, Run tests using -> IntelliJ IDEA로 변경(조금 빠름)
    • 스프링 부트 3.2 부터 스프링이 지원하는 일부 애노테이션의 동작방식이 변경되어 애노테이션을 생략할 때 파라미터를 인식하지 못하는 문제가 있어 build를 Gradle로 설정하는 것을 권장함


2. 비즈니스 요구사항과 설계

1) 회원

  • 회원 가입, 조회 기능
  • 회원등급 - 일반, VIP
  • 회원 데이터  - 자체DB, 외부 시스템 연동(미확정)

2) 주문과 할인 정책

  • 회원 - 상품 주문
  • 회원등급에 따른 차등 적인 할인 정책 적용
  • 할인정책 - 모든 VIP는 1000원 고정 할인 적용(나중에 변경 가능성 있음)
  • 할인정책은 변경 가능성이 높음 - 기본 할인 정책은 오픈 전까지 미정, 최악의 경우 할인 적용 안할 수 있음(미확정)

3. 회원 도메인 설계 

1) 회원 도메인 협력 관계 설정

 

2) 회원 클래스 다이어 그램 - 정적

3) 회원 객체 다이어 그램(실제 인스턴스끼리의 참조) 

  • 동적 다이어그램


4. 회원 도메인 개발

1) 회원 엔터티

(1) 회원등급 - enum

  • member 패키지를 생성 후 작성
public enum Grade {
    BASIC,
    VIP
}

 

(2) 회원 - class

  • 변수 선언 및 생성자, 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;
    
    // getter, setter 생성
}

2) 회원 저장소

(1) 회원 저장소 인터페이스

  • 회원을 저장하는 save메서드와 회원의 id값을 갖고 회원을 조회하는 findById메서드를 선언
public interface MemberRepository {
    void save(Member member);
    Member findById(Long memberId);
}

 

(2) 메모리 회원 저장소 구현체

  • 데이터베이스가 아직 확정이 되지 않았으므로 가장 단순한 메모리 회원 저장소를 구현해서 우선 개발을 진행하는 컨셉
  • 회원 저장소 인터페이스를 구현하는 클래스
  • 인터페이스 메서드들의 동작을 구현

** 참고

  • 실무에서는 HashMap은 동시성 이슈가 발생할 수 있기 때문에 ConcurrentHashMap을 이용해야함
public class MemoryMemberRepository implements MemberRepository{

    // 원래는 동시성 문제 때문에 ConcurrentHashMap을 써야함
    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);
     }
}

3) 회원 서비스

(1) 회원 서비스 인터페이스

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

 

(2) 회원 서비스 구현체 

  • 메모리 리포지토리 객체를 생성하여, 비즈니스 로직인 회원 가입과 회원 조회 기능을 구현
// 구현체가 하나만 있을 때 클래스명 뒤에 Impl이라고 많이 씀(관례)
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);
    }
}

5. 도메인 실행과 테스트

1) 회원 도메인

(1) 회원 가입 main

  • ~/hello.core의 경로에 작성
  • 회원객체를 1개를 생성한 후 main메서드에서 MemberServiceImpl의 객체를 생성하여 비즈니스로직의 동작들을 확인
  • 그러나 이렇게 애플리케이션 로직으로 직접 테스트 하는 것은 좋은 방법이 아니므로 JUnit 테스트를 사용해야함
public class MemberApp {

    //main method에서 테스트 진행
    public static void main(String[] args) {
        MemberService memberService = new MemberServiceImpl();

        Member member = new Member(1L,"memberA", Grade.VIP);
        memberService.join(member);

        Member finMember = memberService.findMember(1L);
        System.out.println("new member = " + member.getName());
        System.out.println("find Member = " + finMember.getName());
    }
}

 

(2) 회원 가입 테스트(JUnit Test)

  • ~/test/java/hello.core 경로에 member 패키지를 생성 후 class를 작성
  • 테스트에서 생성한 member를 join메서드로 저장한 후 findMember 메서드로 찾은 후 Assertions.assertThat으로 member와 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(1L);

        //then
        Assertions.assertThat(member).isEqualTo(findMember);
    }
}

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

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

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

** 참고

  • 실제로는  주문데이터를 DB에 저장하겠지만 이번 예제에서는 생략하고 주문 결과만 반환

2) 주문 도메인 전체와 주문 도메인 클래스 다이어그램

  • 역할과 구현을 분리하여 자유롭게 구현 객체를 조립할 수 있도록 설계
  • 저장소와 할인 정책을 유연하게 변경할 수 있음

좌) 주문 도메인 전체 / 우) 주문 도메인 클래스 다이어 그램

 

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

  • 회원을 메모리 / DB에서 조회하고 할인 정책을 변경하여도 주문 서비스를 변경하지 않아도 된다

좌) 메모리 에서 회원을 조회 / 우) DB에서 회원을 조회 / 협력 관계를 그대로 재사용이 가능


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

1) 할인 정책

(1) 할인 정책 인터페이스

  • discount 패키지를 생성 후 할인 메서드를 가진 할인 정책 인터페이스 생성
  • 할인 메서드에는 회원과 가격정보가 필요함
package hello.core.discount;

public interface DiscountPolicy {
    int discount(Member member, int price);
}

 

(2) 정액 할인 정책 구현체

  • VIP = 1000원 할인
  • VIP가 아니면 할인 없음
package hello.core.discount;

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

    @Override
    public int discount(Member member, int price) {
        // Enum타입은 비교시 == 써야함
        if (member.getGrade() == Grade.VIP) {
            return discountFixAmount;
        } else {
            return 0;
        }
    }
}

2) 주문

(1) 주문 엔터티

  • order패키지 추가 후 작성
  • 변수, 생성자, getter setter작성
  • 아이템 가격에서 할인 가격을 빼서 반환하는 calculatePrice 메서드 정의
  • IDE의 도움을 받아 자동생성 기능을 사용하여 toString()를 생성
package hello.core.order;

public class Order {
    // 변수선언
    private Long memberId;
    private String itemName;
    private int itemPrice;
    private int discountPrice;
    
    public int calculatePrice() {
        return itemPrice - discountPrice;
    }
    
    // getter, setter, 필드 초기화 생성자 생략

    // toString 생성(IDE 자동 생성 기능)
    @Override
    public String toString() {
        // 구현 코드 생략
    }
}

 

(2) 주문 서비스 인터페이스

  • 주문 기능을하는 createOrder메서드 선언
package hello.core.order;

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

 

(3) 주문 서비스 구현체

  • 메모리 회원 저장소와 고정 할인 정책을 객체로 생성하고 createOrder메서드를 구현하는 구현체 생성
  • 저장소에서 회원을 조회하고 등급을 확인하고 할인을 적용한뒤 주문 객체를 생성하여 새로운 주문을 생성하여 반환
package hello.core.order;

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

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

1) 주문과 할인 정책

(1) 실행(main)

  • 애플리케이션에서 실행하는 좋지 않은 테스트이며 연습이기에 회원 정책과 동일하게 작성하여 테스트
  • 출력결과를 확인해보면 주문을 생성할때 넘긴 정보과 출력 정보가 동일하게 주문이 잘 생성된 것을 확인할 수 있음
package hello.core;

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.toString() = " + order.toString());
        System.out.println("order.calculatePrice = " + order.calculatePrice());
    }
}

/* 출력결과 
order.toString() = Order{memberId=1, itemName='itemA', itemPrice=10000, discountPrice=1000}
order.calculatePrice = 9000
*/

 

(2) 주문과 할인 정책 테스트(JUnit Test)

  • 애플리케이션에서 수행한 테스트를 Junit으로 검증하는 테스트
  • 동일하게 정상적으로 테스트가 통과됨
package hello.core.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);
    }
}

3) 순수 자바로 구현한 애플리케이션의 문제점

(1) 요구사항의 변경으로 할인 정책이 변경됨

  • 다른 저장소로 변경이 되었을 때 OCP원칙과 DIP원칙을 지키며 변경할 수 없음
  • 의존관계가 인터페이스 뿐아니라 구현까지 모두 의존하는 문제점이 존재함
  • 다형성을 활용하여 역할과 구현을 구분하였지만 할인 정책을 정률 할인 정책으로 변경 하였을 때 클라이언트에 영향이 없도록 변경이 가능한지 객체지향적으로 잘 개발 되었는 것인가에 대한 의문이 남아있음