[SpringBoot/Java/03.회원관리 예제]

안지은·2023년 1월 19일
0

회원관리 예제를 만들기 위한 전체 과정은 아래와 같다.

  1. 비즈니스 요구사항 정리
  2. 회원 도메인과 회원 도메인 객체를 저장하고 불러올 수 있는 리퍼지토리 만들기
  3. 회원 리포지토리가 정상 동작하는지 확인하기 위한 테스트 케이스 작성
  4. 비즈니스 로직이 있는 회원 서비스를 개발
  5. 서비스가 정상적으로 동작하는지 테스트

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

  • 데이터 : 회원 ID, 이름
  • 기능 : 회원 등록, 조회
  • 가상의 시나리오 : 아직 데이터 저장소(DB)가 선정되지 않음. 데이터 저장소는 RDB, NoSQL 등 다양한 저장소를 고민 중인 상황으로 가정.

일반적인 웹 애플리케이션 계층 구조 (출처 - 인프런)

컨트롤러 : 웹 MVC의 컨트롤러 역할
도메인 : 회원, 주문, 쿠폰처럼 데이터베이스에 저장하고 관리되는 비즈니스 도메인 객체
서비스 : 비즈니스 도메인 객체를 가지고 핵심 비즈니스 로직을 구현한 객체 (ex) 중복 가입 불가)
리포지토리 : 데이터베이스에 접근하여 도메인 객체를 DB에 저장하고 관리

회원 비즈니스 로직에는 회원 서비스가 존재한다. 회원 레퍼지토리는 interface로 구현 클래스를 변경할 수 있도록 설계하고, 아직 데이터 저장소가 선정되지 않아 우선 가벼운 메모리 기반의 데이터 저장소 구현체를 만들 것이다.

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

Member.java - 회원 객체

package hello.hellospring.domain;

public class Member {

    private Long id;  //우선 시스템에서 임의로 설정한 id
    private String name;

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

MemberRepository.java - 회원 리포지토리 인터페이스

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

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

public interface MemberRepository {
    //리포지토리 안의 4가지 기능
    Member save(Member member);  //회원을 저장
    Optional<Member> findById(Long id);  //id로 회원을 찾음.
    Optional<Member> findByName(String name);  //name으로 회원을 찾음.
    List<Member> findAll();  //지금까지 저장된 모든 회원 리스트를 반환.
}

MemoryMemberRepository.java - 회원 리포지토리 메모리 구현체

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

import java.util.*;

public class MemoryMemberRepository implements MemberRepository {

    private static Map<Long, Member> store = new HashMap<>();
    private static long sequence = 0L;  //0,1,2...의 key값을 생성

    @Override
    public Member save(Member member) {
        member.setId(++sequence); //member 저장시 sequence 값을 올려가며 id를 setting.
        store.put(member.getId(), member);  //member의 id를 저장
        return member;  //저장된 결과 반환
    }

    @Override
    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(store.get(id));  //Optional-null이어도 감싸서 반환.
    }

    @Override
    public Optional<Member> findByName(String name) {
        //member의 name과 인자로 받은 name이 일치하는지 비교
        return store.values().stream()
                .filter(member -> member.getName().equals(name))
                .findAny();
    }

    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());  //member들을 반환
    }
}

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

이렇게 작성한 코드가 제대로 동작하는지 확인하기 위해 테스트 케이스를 작성한다. main 메소드나 컨트롤러를 통해 테스트를 하는 방법은 시간이 오래 걸리며 반복 실행을 하는 것과 여러 테스트를 한 번에 실행하기가 어렵다. 이러한 문제 해결을 위해 자바에서는 JUnit이라는 프레임워크로 테스트 코드를 만들고 실행하여 테스트를 진행한다.

테스트 코드는 main이 아닌 test 아래에 작성한다.

아래와 같이 테스트 코드를 작성하여 각 메소드가 정상적으로 실행되는지 테스트한다.

package hello.hellospring.repository;

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

import java.util.List;

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

class MemoryMemberRepositoryTest {

    MemoryMemberRepository repository = new MemoryMemberRepository();

    @Test  //아래 메소드를 실행시킬 수 있음.
    public void save() {
        Member member = new Member();
        member.setName("spring");


        repository.save(member);

        Member result = repository.findById(member.getId()).get();

        //검증 (member와 리포지토리에서 꺼낸 값이 같다면 true)
        assertThat(member).isEqualTo(result);  //성공하면 초록불
    }

    @Test
    //이름으로 회원을 찾음.
    public void findByName() {
        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);
    }
}

이때, 테스트는 순서 상관없이 메소드별로 따로 진행한다. 따라서 위 테스트들을 한 번에 진행하여 findAll()이 먼저 실행되는 경우, findByName()에서 이미 생성된 객체를 또 만들고 있으니 오류가 발생한다.

이러한 경우를 방지하기 위해 테스트를 하나 끝내고 나면 data를 clear해줘야 한다. 따라서 main 아래 있는 memory repository에 객체를 비워주는 메소드를 작성하고, 테스트 코드에는 테스트가 끝날 때마다 repository를 지워주는 코드를 추가시켜준다.

MemoryMemberRepository.java

 public void clearStore() {
        store.clear();
    }

MemoryMemberRepositoryTest.java

@AfterEach  //하나의 메소드 실행이 끝날 때마다 어떤 동작을 하도록 함.
    public void afterEach() {
        repository.clearStore();
    }

즉, 테스트는 서로 의존관계 없이 설계가 되어야 한다. 그러기 위해선 하나의 테스트가 끝나면 저장소나 공용 data를 깔끔하게 지워줘야 한다.

💡 회원 서비스 클래스 작성

회원 리포지토리와 도메인을 활용해 실제 비즈니스 로직을 작성해보자. 먼저 service 패키지를 생성 후 그 아래에 MemberService 클래스를 작성한다.

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;

    //memberRepository를 외부에서 넣도록 함. (Dependency Injection)
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    //회원가입 (임의로 id를 반환하는 것으로 설정)
    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> findMember() {
        return memberRepository.findAll();  //findAll()의 리턴 타입:List
    }

    //아이디로 특정 회원 조회
    public Optional<Member> findOne(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

💡 회원 서비스 테스트

작성했던 회원 서비스 클래스의 정상적인 동작 확인을 위해 테스트 케이스를 작성해보자. 회원 서비스 클래스에서 Ctrl+Shift+T를 누르면 test 아래에 자동으로 테스트를 생성할 수 있다.

참고로, 테스트를 작성할 땐 given(주어진 이 data를 기반으로), when(이 동작을 실행했을 때), then(이 결과가 나와야 함.)으로 나눠서 작성해보자. (나눠지지 않을 땐 스스로 적절히 변형해서 작성하자.)

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.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {

    MemberService memberService;
    MemoryMemberRepository memberRepository;

    @BeforeEach  //각 테스트를 실행하기 전에 수행됨.
    public void beforeEach() {
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository);
    }

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

        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
        //then
    }

    @Test
    void findMember() {
    }

    @Test
    void findOne() {
    }
}
profile
공부 기록용

0개의 댓글