개발하는 자몽

스프링 부트 공부 (5), 회원 관리 예제 - 백엔드 본문

Java & Kotlin/Spring

스프링 부트 공부 (5), 회원 관리 예제 - 백엔드

jaamong 2022. 1. 4. 17:09
 

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

스프링 입문자가 예제를 만들어가면서 스프링 웹 애플리케이션 개발 전반을 빠르게 학습할 수 있습니다., 스프링 학습 첫 길잡이! 개발 공부의 길을 잃지 않도록 도와드립니다. 📣 확인해주세

www.inflearn.com

위 강의를 바탕으로 작성 중입니다.

 

비즈니스 요구사항 정리(쉬운 버전)

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

 

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

출처: 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술

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

 

회원 도메인, 리포지토리 만들기

회원 도메인

domain 패키지 - Member 객체

  • ID : 이메일 값 → 고객이 정하는 것이 아닌 구분을 위해 시스템이 저장하는 ID(식별자)
  • name : 이름

  • private 객체 사용을 위한 getter, setter 생성
  • IntelliJ getter, setter 단축키 : Alt + Insert (윈도우)

 

리포지토리

repository 패키지 - MemberRepository 인터페이스

  • 회원 객체를 저장하는 저장소
  • 인터페이스로 한 이유 : 아직 데이터 저장소가 선정되지 않아서, 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계

repository 패키지 - MemoryMemberRepository 클래스

package Hello.hellospring.repository;

import Hello.hellospring.domain.Member;
import java.util.*;

public class MemoryMemberRepository implements MemberRepository { //Alt+Enter로 메소드 전부 생성

    /* 저장(save)를 할 때 어딘가에 저장해야함 : Memory */
    //실무에서는 동시성 문제가 있을수도 있어서 공유되는 변수일 때는 ConcurrentHashMap사용
    private static Map<Long, Member> store = new HashMap<>();
    private static long sequence = 0L; // 0, 1, 2, ... 키 값 생성

    @Override
    public Member save(Member member) { //사용자 저장
        member.setId(++sequence); //sequence 값 1 증가시키기
        store.put(member.getId(), member); //ID 세팅 후 저장장
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        //결과가 없으면 null을 반환할텐데
        //Optional.ofNullable로 감싸서 반환하면 클라이언트에서 처리(?) 가능
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public Optional<Member> findByName(String name) {
        return store.values().stream()
                .filter(member -> member.getName().equals(name)) //인자로 넘어온 name이 같은 경우에만 filtering
                .findAny(); //찾으면 반환
    }

    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }
}

 

테스트는 어떻게 할까?

테스트 케이스 작성

  • 내가 원하는 대로, 정상적으로 동작하는지 검증
  • 코드를 코드로 검증
  • 각 메서드 테스트 시 메서드 실행 순서가 결과에 영향을 미쳐서는 안 됨(의존관계 X)
    • 어떤 메서드가 실행되면서 리포지토리에 데이터 저장되고 다른 메서드 실행 시 결과에 영향을 줄 수도 있음
    • @AfterEachclear() 함수를 활용해서 리포지토리를 각 메서드 테스트 실행 후에 비우자
  • JUnit 프레임워크로 테스트 실행

test > java > Hello.hellospring > repository - 테스트하고자하는기능Test

 

save 메서드 테스트

package Hello.hellospring.repository;

import Hello.hellospring.domain.Member;
import org.junit.jupiter.api.Test;

//다른데에서 쓸 것이 아니기 때문에 굳이 public으로 안해도 됨
class MemoryMemberRepositoryTest {
    MemoryMemberRepository repository = new MemoryMemberRepository();

    /**
     * @Test 어노테이션 :
     * org.junit.jupiter api
     * 해당 annotaion을 추가함으로써 메소드를 그냥 실행할 수 있음
     */
    @Test
    public void save() { //save 테스트
        Member member = new Member();
        member.setName("jaamong");

        //리포지토리에 save
        repository.save(member); //save 메소드에서 Id 세팅

        //제대로 저장이 됐는지 확인
        Member result = repository.findById(member.getId()).get(); //반환 타입 : Optional -> Optional에서 값을 꺼낼 때는 get()으로 가능

        //검증 : 저장한 거랑 db에서 꺼낸 거랑 동일하면 true
        //단순하게
        System.out.println("result = " + (result == member));
    }
}

 

 

결과 1(단순한 방식의 검증)

save 메소드 테스트 코드 실행 결과

 

매번 저렇게 출력할 수는 없으니 Assertions를 사용하자

Assertions를 이용한 검증

기대하는 결과 : 저장했던 member가 findById 하면 나와야 함

 

결과 2(Assert 사용 방식의 검증)

성공 화면

  • 출력 X
  • 왼쪽을 보면 초록색으로 체크 표시가 보임 → 성공

 

실패 화면

  • null을 집어넣으면 빨간불
  • Expected를 보면 기대한 값이 나오는데 해당 값을 주지 않고 Actual : null을 줘서 해당 난리가 남

 

findAll 메서드 테스트

findAll 메소드 테스트 코드 실행 결과

  • save 2명 : jaamong, aejaamong
  • result 사이즈와 actual 2 비교 → 성공

 

회원 서비스 개발, 테스트

개발

service 패키지 - MemberService 클래스

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 {

    //TODO : 회원 서비스를 만드려면 회원 리포지토리가 있어야 함
    private final MemberRepository memberRepository = new MemoryMemberRepository();

    /**
     * 회원가입
     */
    public Long join(Member member){
        //같은 이름이 있는 중복 회원은 안됨
        //findByName 리턴 형태가 Optional Member이니까 => Optional Member.ifPresent ~~~
        validatedDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();
    }

    private void validatedDuplicateMember(Member member) { //메소드 추출 : ctrl + alt + m
        memberRepository.findByName(member.getName())
                .ifPresent(m -> { //ifPresent : null이 아니라 만약 값이 있으면, m : member => member에 값이 있으면
                    throw new IllegalStateException("이미 존재하는 회원입니다."); // null이 아니라 만약 값이 있으면 throw
                });
    }

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

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

 

테스트

쉽게 테스트 코드 만들기

  • 테스트 코드를 만들기 원하는 클래스 클릭 후
  • 단축키 : ctrl + shift + t 
  • Create New Test 뜸 (→ Testing library : JUnit5 선택했음)

회원가입(Join) 테스트, 정상 플로우

 

중복 회원인 경우, 예외 플로우

지금은 중복되는 이름이 없어서 성공

 

 

테스트할 때 DB랑 객체 선언 관련해서 더 신경 써야 되는 게 많은데 일단 보류..

Comments