스프링 핵심 원리(기본편) 강의정리 - 2 (멤버편)

2023. 9. 16. 16:57Spring/Spring 김영한

📌 목차 (이번 챕터는 예제 만들기이다.)


  • 프로젝트 생성
  • 비즈니스 요구사항과 설계
  • 회원 도메인 설계
  • 회원 도메인 개발
  • 회원 도메인 실행과 테스트
  • 주문과 할인 도메인 설계
  • 주문과 할인 도메인 개발
  • 주문과 할인 도메인 실행과 테스트

👊 가봅시다!..


❓프로젝트 생성방법

스프링부트스타터

  • Project : Gradle -Groovy
  • Language : Java
  • Spring Boot : 2.xx (3.00 version 이상부터는 JAVA 17 이상을 사용해야 합니다.
  • Dependencies : 선택 X(지금 예제에서는 스프링을 사용하지 않는다. 순수 자바로만 사용)

각 항목들을 체크해 주었다면 GENERATE를 눌러 프로젝트 파일을 다운로드하고 IDE에서 프로젝트를 오픈합니다!
초기에는 bulid 하느라 시간이 조금 걸릴 수 있음.

 


CoreApplication.java를 실행해서 테스트합니다.

아래처럼 출력이 되면 성공입니다!

 

 

 

 

 

비즈니스 요구사항과 설계


  • 회원
    • 회원을 가입하고 조회할 수 있다.
    • 회원은 일반과 VIP 두 가지 등급이 있다.
    • 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)
  • 주문과 할인 정책
    • 회원은 상품을 주문할 수 있다.
    • 등급에 따라 할인 정책을 적용할 수 있다.
    • 할인 정책은 VIP만 1000원을 할인해 주는 고정 금액 할인을 적용해 달라. (변경가능)
  • 아직 미정인 부분들이 많다. 객체지향이 필요한 이유이다. 추후에 부품을 갈아 끼우듯이 쉽게 변경할 수 있게끔 설계해야 한다!

 

 

 

 

❓ 회원 도메인 설계


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

클라이언트는 회원을 가입하고 조회할 수 있어야 하지만, 내부의 저장소는 알 필요가 없다. 회원 저장소는 아직 미정이다.

 

 

 

 

❓회원 도메인 개발


회원 엔티티

회원 등급 클래스

일반회원과 VIP를 구분하기 위해 enum을 이용했다.

package hello.spring_core.member;

    public enum Grade {
        BASIC,
        VIP
    }

회원 엔티티

  • 회원 이름, 회원 ID , 회원등급의 정보를 저장하고 불러옵니다!
    • get, set함수, 생성자를 같이 구현합니다.
package hello.spring_core.member;

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 void setId(Long id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setGrade(Grade grade) {
        this.grade = grade;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public Grade getGrade() {
        return grade;
    }
}

 

 

 

 

 

 

 

❓ 회원 저장소


회원 저장소 인터페이스

-> 인터페이스의 공통속성이나 기능을 적어두고 SubClass에서 이를 상속하여 재사용하는 방식입니다.

package hello.spring_core.member;

public interface MemberRepository {
    void save(Member memeber);

    Member findByID(Long mebmerId);
}

 

 

❓ 메모리 회원 구현체 (class)

아직 데이터베이스가 확정되지 않았다! 그러므로 가장 단순한, 메모리 회원 저장소를 구현.

HashMap을 이용해서 저장하고, 회원가입과 조회기능을 위한 save()와 join() 함수를 오버라이딩 하였다!

package hello.spring_core.member;

import org.springframework.boot.autoconfigure.AutoConfigurationMetadata;

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.spring_core.member;

public interface MemberService {
    void join(Member member);

    Member findMember(Long memberId);
}

 

 

 

회원 서비스 구현체

인터페이스를 상속하고 메서드 오버라이딩을 통해 join과 findMember함수를 재구성한다. 또한 인터페이스의 구현체가 하나뿐일 때 보통 인터페이스 뒤에 Impl을 붙인다고 하셨다.

package hello.spring_core.member;

public class MemberServiceImpl implements MemberService {
    private  MemberRepository memberRepository;

    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findByID(memberId);

    }
}

 

 

 

 

❓ 회원 도메인 실행과 테스트


회원 도메인 - main에서 회원가입을 하고 findMember를 통해 객체를 return 받고 출력하여 비교해 보자.

package hello.spring_core;

import hello.spring_core.member.*;

public class MemberApp {
    public static void main(String[] args) {
        MemberService memberService = new MebmerServiceImpl();
        Member member = new Member(1L, "memberA", Grade.VIP);
        memberService.join(member);

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

}

정상적으로 출력됐다! 하지만, 이런 식으로 애플리케이션 로직으로 테스트하는것은 좋은 방법이 아니다 JUnit 테스트를 사용하자. (일일이 눈으로 확인해봐야 하기 때문)

 

 

 

 

 

❓회원 도메인 - 회원 가입 테스트


-UnitTest란? : 아래 Test코드와 같이 순수 자바로만 테스트 하는 단위를 말합니다. 이런 단위 테스트는 실행시간이 짧기 때문에 UnitTest를 잘 짜는 것이 중요하다고 하심!

  • Assertions.assertThat(member) : org.assertj.core.... 을 import 해야 한다. (JUNIT 말고)
    • assertion을 제공해 주는 라이브러리로 에러 메시지와 테스트 코드의 가독성을 매우 높여주고 각자 좋아하는 IDE에서 쓰기 굉장히 쉽다.
  • 예시코드
@Test void a_few_simple_assertions() {
    assertThat("The Lord of the Rings").isNotNull() 
    .startsWith("The") 
    .contains("Lord") 
    .endsWith("Rings"); 
}

한눈에 어떤 것을 하려는지 보일 정도로 가독성이 좋다. 테스트 코다는 assertion으로 시작해서 assertion으로 끝난다고 한다.

아래와 같이 StaticImport를 하면 클래스 이름 없이 바로 사용할 수 있어 더욱 좋다!

 

테스트코드 작성 방법

  • //given :어떤 것이 주어졌을 때 (이 데이터를 기반)
  • //when : 어떤 상황에서 (실행시키고자 하는 내용)
  • //then : 이런 결과가 나와야 한다. (기댓값)
package hello.spring_core.member;


import hello.spring_core.AppConfig;
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);
    }
}

 

 

실행화면

이렇게 초록불로 성공 시에 출력해 주고 에러가 나면 빨간색으로 표시해 준다!

 

 

 

회원 도메인 설계의 문제점


  • 다른 저장소로 변경하려고 할 때 OCP원칙을 잘 준수했나?
  • DIP를 잘 지키고 있을까요?
  • 의존관계가 인터페이스뿐만 아니라 구현까지 모두 의존하고 있음!
    • MemberServiceImpl에서 저장소인 MemoryMemberRepository를 new로 동적할당 하고 있지만, 이는 인터페이스가 아닌 class에서 의존관계를 결정하므로 인터페이스에 의존하는 것이 아닌 하위 클래스에 의존하고 있기 때문에 , DIP를 위반했다고 볼 수 있다. 뭔가 더 필요하다! -> 이후에 다룰 예정.