Spring/[인프런] 스프링 입문 - 김영한

[Spring] 회원 관리 예제 - 백엔드 개발

송테이토 2022. 11. 19. 13:52

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8/dashboard

 

[무료] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링 웹 애플리케이션 개발 전반을 빠르게 학습할 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

김영한님의 인프런 스프링 입문 - 코드로 배우는 스프링 부트 강의를 듣고 정리한 공부목적의 글입니다.

 

  • 비즈니스 요구사항 정리
  • 회원 도메인과 리포지토리 만들기
  • 회원 리포지토리 테스트 케이스 작성
  • 회원 서비스 개발
  • 회원 서비스 테스트

1. 비즈니스 요구사항 정리

  • 데이터: 회원ID, 이름
  • 기능: 회원 등록, 조회
  • 아직 데이터 저장소가 선정되지 않음(가상의 시나리오)

일반적인 웹 애플리케이션 계층 구조

컨트롤러: 웹 MVC의 컨트롤러 역할 서비스: 핵심 비즈니스 로직 구현 리포지토리: 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리 도메인: 비즈니스 도메인 객체, 예) 회원, 주문, 쿠폰 등등 주로 데이터베이스에 저장하고 관리됨

클래스 의존관계

아직 데이터 저장소가 선정되지 않아서, 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계 데이터 저장소는 RDB, NoSQL 등등 다양한 저장소를 고민중인 상황으로 가정 개발을 진행하기 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용

2. 회원 도메인과 리포지토리 만들기

domain.MemoryMemberRepository.java

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

import java.util.*;

/*
 * 동시성 문제가 고려되어 있지 않음, 실무에서는 ConcurrentHashMap, AtomicLong 사용 고려
 */

public class MemoryMemberRepository implements MemberRepository {
    //저장을 하는 곳이 필요.Map 사용, 키는 회원의 아이디이므로 Long, 값은 Member
    private static Map<Long, Member> store = new HashMap<>(); //실무에서는 동시성 문제가 있을 수 있자만 예제니까
    private static long sequence = 0L; //실무에서는 동시성 문제 고려해서 AtomicLong

    @Override

    public Member save(Member member) {
        member.setId(++sequence);//아이디 세팅하고
        store.put(member.getId(), member); //store에 저장. Map에 저장됨

        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        //store에서 꺼낸다.

        //근데 값이 없으면 null이 나옴. 그렇기 때문에 Optional.ofNullable() 사용
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public Optional<Member> findByName(String name) {
        //member.getName()과 파라미터 name과 같은지 비교
        //같으면 필터링! 찾으면 반환해준다!!
        // 결과가 없다면? Optional에 null이 포함돼서 반환
        return store.values().stream().filter(member -> member.getName().equals(name))
                .findAny();//Optional로 결과 찾음
    }

    @Override
    public List<Member> findAll() {
        //Map인데 반환은 List로 되어있음
        //실무에서는 List를 많이 사용
        return new ArrayList<>(store.values()); //member들을 반환해줌
    }
}

회원 리포지토리 테스트 케이스 작성

개발한 기능을 실행해서 테스트 할 때 자바의 main 메서드를 통해서 실행하거나, 웹 애플리케이션의 컨트롤러를 통해서 해당 기능을 실행한다. 이러한 방법은 준비하고 실행하는데 오래 걸리고, 반복 실행하기 어렵고 여러 테스트를 한번에 실행하기 어렵다는 단점이 있다.

자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결한다.

test/hello.helloSpring/repository/MemoryMemberRepositoryTest.java

보통 Test를 붙여주는게 관행

save : Test

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class MemoryMemberRepositoryTest {

    MemoryMemberRepository repository = new MemoryMemberRepository();

    @Test //import
    public void save() {
        Member member = new Member();
        member.setName("spring");
        repository.save(member);

        //내가 넣은게 제대로 들어갔나 보기
        repository.findById(member.getId()).get();
        //반환 타입이 optional임. 값 꺼내려면 get으로 꺼냄(물론 get으로 바로꺼내는건 좋은건 아님)
        Member result = repository.findById(member.getId()).get();

        //검증!
        Assertions.assertEquals(member, result); //기대하는 것
    }
}

Assertions.assertEquals(member, result); →값이 같은지!!!!

만약 틀린 값이라면?

findByName Test

@Test
    public void findByName(){

        //Spring1 , Spring2 라는 회원이 가입됨
        Member member1 = new Member();
        member1.setName(("Spring1"));
        repository.save(member1);

        Member member2 = new Member();
        member2.setName(("Spring2"));
        repository.save(member2);

        Member result = repository.findByName("Spring1").get();

        assertThat(result).isEqualTo(member1);
    }

findAll() Test

@Test
    public void findAll() {
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);

        Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2);

        List<Member> result = repository.findAll();

        assertThat(result.size()).isEqualTo(2);

    }

만약 assertThat(result.size()).isEqualTo(3); 으로 바꾸면

org.opentest4j.AssertionFailedError: 
expected: 3
 but was: 2
Expected :3
Actual   :2

이런 오류가 뜸!

기대한건 3개인데 사실 2개 밖에 없다

Test가 끝나면 clear을 해줘야함

java부분에

//Test가 끝나면 sotre을 clear 해줌
    public void clearStore(){
        store.clear();
    }

추가

테스트 부분에

//메서드 실행이 끝날때마다 실행.
    // Test가 끝나면 clear. 콜백함수
    @AfterEach
    public void afterEach() {
        repository.clearStore();
    }

추가!

테스트 코드는 정말 정말 중요하다!!!!!!!!!!!

최종 테스트 코드 - MemoryMemberRepositoryTest.java

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.assertj.core.api.Assertions.*;

public class MemoryMemberRepositoryTest {

    MemoryMemberRepository repository = new MemoryMemberRepository();

    //메서드 실행이 끝날때마다 실행.
    // Test가 끝나면 clear. 콜백함수
    @AfterEach
    public void afterEach() {
        repository.clearStore();
    }

    @Test //import
    public void save() {
        Member member = new Member();
        member.setName("spring");
        repository.save(member);

        //내가 넣은게 제대로 들어갔나 보기MemoryMemberRepositoryTest
        repository.findById(member.getId()).get();
        //반환 타입이 optional임. 값 꺼내려면 get으로 꺼냄(물론 get으로 바로꺼내는건 좋은건 아님)
        Member result = repository.findById(member.getId()).get();

        //검증!
        //실무에서는 빌드 툴이랑 엮어서 함
        assertThat(member).isEqualTo(result);
    }

    @Test
    public void findByName() {

        //Spring1 , Spring2 라는 회원이 가입됨
        Member member1 = new Member();
        member1.setName(("Spring1"));
        repository.save(member1);

        Member member2 = new Member();
        member2.setName(("Spring2"));
        repository.save(member2);

        Member result = repository.findByName("Spring1").get();

        assertThat(result).isEqualTo(member1);
    }

    @Test
    public void findAll() {
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);

        Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2);

        List<Member> result = repository.findAll();

        assertThat(result.size()).isEqualTo(2);

    }
}

3. 회원 서비스 개발

\

실제 비즈니스 로직

memberRepository.findByName(member.getName());

crtl + alt + v

Optional<Member> byName = memberRepository.findByName(member.getName());

로 바뀐다

  • throw new IllegalStateException("이미 존재하는 회원입니다.");
  • result.orElseGet() : 값이 있으면 꺼내고 없으면 메서드를 수행해!
package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;

import java.util.Optional;

public class MemberService {
    private final MemberRepository memberRepository = new MemberRepository();

    /*
     * 회원 가입
     */
    public Long join(Member member) {
        //같은 이름이 있는 중복 회원X

        //멤어에서 파인드바이네임으로 찾아야함.
        //null일 가능성이 있다면 Optional 로 감싸주기
        Optional<Member> result = memberRepository.findByName(member.getName());

        //멤버 m 에 값이 있다면 문구 띄어주기
        result.ifPresent(m -> {
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        });

        memberRepository.save(member);
        return member.getId();
    }
}
  • Shift+Ctrl+Alt+T - Refactor This
  • 현재 화면의 리팩토링 요소(rename, move class 등)를 활용합니다.
package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;

import java.util.Optional;

public class MemberService {
    private final MemberRepository memberRepository = new MemberRepository();

    /*
     * 회원 가입
     */
    public Long join(Member member) {

        validateDuplicateMember(member); //중복 회원 검증
        memberRepository.save(member);
        return member.getId();
    }

    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                .ifPresent(m -> {
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        });
    }
}

최종 비즈니스 메서드 - MemberService.java

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;

import java.util.List;
import java.util.Optional;

public class MemberService {
    private final MemberRepository memberRepository = new MemoryMemberRepository();

    /*
     * 회원 가입
     */
    public Long join(Member member) {

        validateDuplicateMember(member); //중복 회원 검증
        memberRepository.save(member);
        return member.getId();
    }

    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                .ifPresent(m -> {
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        });
    }

    /*
     * 전체 회원 조회
     */
    public List<Member>findMembers(){
        return memberRepository.findAll();
    }

    public Optional<Member> findOne(Long memberId){
        return memberRepository.findById(memberId);
    }
}

4.

[인텔리제이 / Intelli J] 인텔리제이에서 테스트...

  • alt Enter
  •  

이러면 이렇게 자동으로 껍데기가 만들어진다.

 

테스트 해보기

테스트는 한글로 바꿔도 된다!!!!!!!

빌드할 때 실제 코드에 포함되지 않음

테스트라는건 사실 이런 상황이 주어졌을 때 하는것이다.

@Test
    void 회원가입() {
        //given

        //when

        //then
    }

회원가입

@Test
    void 회원가입() {
        //given
        Member member = new Member();
        member.setName("hello");

        //when
        Long saveId = memberService.join(member);

        //then
        Member findMember = memberService.findOne(saveId).get();
        Assertions.assertThat(member.getName()).isEqualTo(findMember.getName());

    }

assertThrows()

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {

    MemberService memberService;
    MemoryMemberRepository memberRepository;

    @BeforeEach
    public void beforeEach(){
        memberRepository = new MemoryMemberRepository();
        memberService= new MemberService(memberRepository);

    }

    //clear 해주기
    @AfterEach
    public void afterEach(){
        memberRepository.clearStore();
    }

    @Test
    void 회원가입() {
        //given
        Member member = new Member();
        member.setName("hello");

        //when
        Long saveId = memberService.join(member);

        //then
        Member findMember = memberService.findOne(saveId).get();
        Assertions.assertThat(member.getName()).isEqualTo(findMember.getName());

    }

    //예외가 잘 터뜨려지는지도 확인하기
    @Test
    public void 중복_회원_예외() {

        //given
        //중복 회원 저장
        Member member1 = new Member();
        member1.setName("spring");

        Member member2 = new Member();
        member2.setName("spring");

        //when
        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));

        Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
//
//        try {
//            memberService.join(member2);
//            fail();
//        } catch (IllegalStateException e) {
//            Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다. TEST");
//        }

        //then

    }

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}