[Spring] 간단한 회원 관리 예제

김유진·2022년 10월 22일
0

Spring

목록 보기
8/12
post-custom-banner

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

먼저 Spring으로 회원관리를 설계하기 위해서는 요구사항을 먼저 정리해야한다.

  • 데이터 : 회원ID, 이름
  • 기능 : 회원 등록, 조회
  • 아직 저장소가 선정되지 않음

위 조건을 기반으로 웹 애플리케이션 구조도 분석해보자.

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

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

클래스 의존관계

  • 아직 데이터의 저장소가 없어서, 간단한 메모리로 우선 인터페이스로 구현 클래스 변경함
  • 데이터 저장소는 RDB, NoSQL등등 다양한 저장소를 고민중임.
  • 개발을 진행하기 위하여 초기 개발 단계는 구현체로 가벼운 메모리 기반의 데이터 사용

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

도메인 만들기


이렇게 폴더 안에 package를 만들어서 먼저 domain을 생성합니다.
domain 안에 Member라는 파일을 만들고, 그 안에 다음과 같이 입력합니다.

package Inflearn_spring.studyspring.domain;

public class Member {
    private Long 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;
    }
}

이렇게 입력하는 이유는 Member라는 객체 안에 우리가 아까 요구사항 분석할 때 회원ID, 이름으로 만든다고 하였기 때문에 id, name을 세팅해 준 것이고 getter와 setter로 기본적인 기능을 구현해놓은 것이다.

레포지토리 만들기 - 인터페이스

그리고 respository 폴더를 만들어서

위와 같이 MemberRepository라는 파일을 만들고 Interface에 체크해줍니다.

package Inflearn_spring.studyspring.repository;

import Inflearn_spring.studyspring.domain.Member;

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

public interface MemberRepository {
    Member save(Member member);
    Optional<Member> findById(Long id);
    Optional<Member> findByName(String name);
    List<Member> findAll();
}

인터페이스에 이렇게 파일을 작성할 수 있습니다.

  • save : 만든 Member의 정보를 저장해줍니다.
  • Optional : findByIdfindByName으로 정보를 찾아왔을 때 null일경우 Optional로 감싸서 반환합니다.
  • List : 만들어진 Member에 대한 전체 리스트를 반환합니다.

레포지토리 만들기 - 구현체

repository라는 파일 안에 MemoryMemberRepository를 만들고 안에 다음과 같은 코드를 넣는다.

package Inflearn_spring.studyspring.repository;

import Inflearn_spring.studyspring.domain.Member;

import java.util.*;

public class MemoryMemberRepository implements MemberRepository  {

    private static Map<Long, Member> store = new HashMap<>();
    private static long sequence = 0L;

    @Override
    public Member save(Member member) {
        member.setId(++sequence);
        store.put(member.getId(),member);
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public Optional<Member> findByName(String name) {
        return store.values().stream()
                .filter((member -> member.getName().equals(name)))
                .findAny();
    }

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

inteface에서 만든 것들이 어떤 역할을 하는지 여기서 기술합니다.
일단 임시 저장소를 Map을 이용하여 store라고 이름을 짓고, 아이디를 하나씩 순차적으로 세기 위하여 sequence라는 변수를 만들었습니다.

  • save함수에서는 유저의 아이디를 ++sequence를 통해 늘려가면서 저장합니다.
  • Optional<Member> findById는 id를 찾아오는데, null일 경우에는 Optional로 감싸서 내보냅니다.
  • Optional<Member> findByName은 이름을 찾는데 하나라도 일치하는 것이 있으면 데려와서 리턴합니다.
  • findAll함수는 모든 리스트를 찾아옵니다.

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

이제 이것이 잘 돌아가는지 테스트케이스를 한번 작성해 보아야겠지요? 개발한 기능을 실행해서 테스트 할 때 자바의 main 메소드를 통하여 실행하거나, 웹 애플리케이션의 컨트롤러를 통해서 해당기능을 실행할 수 있습니다. 이러한 방법은 준비하고 실행하는 데 오래 걸리고, 반복 실행하기 어렵고 여러 테스트를 한번에 실행하기 어렵다는 단점이 있다. 자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결한다.

테스트를 하기 위하여 test 폴더 안에 respository라는 패키지를 하나 생성한다.
그리고 MemoryMemberRepositoryTest라는 파일을 만들고 아래와 같이 코드를 작성한다.

package Inflearn_spring.studyspring.repository;

import org.junit.jupiter.api.Test;

class MemoryMemberRepositroyTest {

    MemoryMemberRepository respository = new MemoryMemberRepository();

    @Test
    public void save() {

    }
}

이렇게 되면 save가 정상 작동하는지 확인할 수 있는데 save 부분만 실행시켜서 코드를 한번 돌려보자.

위와 같이 정상적으로 작동하고 있음을 알 수 있다.

테스트기능을 완전히 구현해서 잘 되는지 확인해보자.

package Inflearn_spring.studyspring.repository;

import Inflearn_spring.studyspring.domain.Member;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

class MemoryMemberRepositroyTest {

    MemoryMemberRepository respository = new MemoryMemberRepository();

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

        respository.save(member);
        Member result = respository.findById(member.getId()).get();
        Assertions.assertEquals(member,result);
    }
}

spring이라는 이름을 가진 유저를 저장하는것이다.
그리고 저장한 후에 findById를 통하여 그 저장한 값을 result에 저장하고, Assertions를 이용하여 내가 구한 값이랑 동일한지 확인해볼 수 있는 것이다.

확인해보면 저장이 잘 된다. 그런데 null값으로 한번 바꾸어 볼까낭?

기대하는 값과 다르기 때문에 test에서 오류가 발생하는 것이다.

다음으로는 findByName도 Test 를 작성해보자.

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

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

        Member result = respository.findByName("spring1").get();
        assertThat(result).isEqualTo(member1);
    }


findByName에 대한 테스트도 문제 없이 진행가능하다!

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

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

        List<Member> result = respository.findAll();
        assertThat(result.size()).isEqualTo(2);
    }

findAll()도 테스트 무난하게 통과!

그런데 갑자기 돌발상황 발생!

잘 먹던 findByName의 테스트를 실패한다..
Spring에서는 Test의 실행 순서를 보장해주지 않는다.
그런데 지금 findAll()이 먼저 실행되었다. findAll()에서 spring1과 spring2를 이미 저장을 해버려서 findByName할 때 이전에 저장한 다른 객체가 자꾸 출력되어서 fail하는 것이다.

그래서 test를 하나 끝내고 나면 깔끔하게 clear를 해주어야 한다!

먼저 이전에 만들었던 repository로 돌아가서 MemoryMemberRepository에 다음 코드를 넣자.

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

이렇게 되고 test파일로 돌아가서 다음 코드를 작성하자.

@AfterEach
    public void afterEach() {
        respository.clearStore();
    }

이거 되게 중요하다. AfterEach가 무엇을 하는 거냐면 콜백함수처럼 어떠한 동작하나가 끝나면 아래 afterEach함수를 수행해 주는 것이다.
이제 확인해보면 Test가 별 문제없이 끝나는 것을 알 수 있다.

테스트는 순서 없이 실행된다는 점 다시한번 기억해두자! 그래서 공용 데이터를 깔끔히 지워주고~ 관계된 관계가 없도록 하자!

3. 회원 서비스 만들기


이렇게 service 프로젝트를 하나 만들어 MemberService라는 class 파일을 하나 만들었다.

실제로 서비스될 아이들이니까 이름도 신중하게 정해줍시다.

그리고 아래와 같이 입력해넣습니다.

import java.util.Optional; public class MemberService {
    private final MemberRepository memberRepository = new MemoryMemberRepository();

이렇게 MemberRepository 에서 생성한 인스턴스를 바탕으로 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("이미 존재하는 회원입니다.");
            });
    }

validateDuplicateMember 라는 메소드를 만들어서 회원가입은 시켜주되, 같은 Name을 가지고 있을시에는 회원가입을 거절시켜준다. 그 이외에는 memberRepositroysave 함수를 통하여 정보를 저장하고 id를 리턴해준다.

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

전체 회원 조회하는 함수와 하의 멤버를 찾아내는 함수이다. 기존에 있던 함수만 이용하면 되므로 매우 간단하다!

회원 서비스 테스트하기

회원 서비스 함수를 직접 치지 않아도 된다. 만들고자 하는 테스트 관련된 함수 위에다가 alt + enter를 한다.

그럼 이렇게 이렇게 바로 테스트를 만들 수 있는 창이 뜬다. 단축키라서 매우매우 간단하다!
그럼 아래와 같이 껍데기가 만들어진다.

이제 회원가입 기능에 대한 테스트를 만들어보자.

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

테스트는 같이 배포되는 것이 아니기 때문에 함수의 이름을 한글로 적어도 무방하다.
여기서 아래 문법을 이용하면 편리하다

  • given
  • when
  • then

주어졌을 때, 이것이 주어졌을 때 어떤 것이 실행되는구나~~ 를 한번에 확인할 수 있도록 주석을 달아놓는것이다.
주석을 깔고 가면 되게 편하다.

일단 given에 멤버가 저장되게 하고,
when에 Id를 가져오게 한다.
마지막에 then은 검증을 해주는데, findOne함수를 통하여 Id를 가져오고, findMemeber로 지정을 해둔다. 이것의 이름이 같은지 안같은지 이제 검증해주면 된다.

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

위는 중복 회원 잡는 코드이다. 만약 이름이 같은 멤버 두명을 가입시키려고 하면 당연히 예외처리가 되어야 하고, 이 코드는 그 예외처리를 수행해주는 것이다. 만약 join을 시켰는데, 다음 코드로 넘어가면 실패하는 것이니까 fail을 해준 것이다. 그리고 오류를 잡아냈다면 asserThat으로 넘어가서 예외처리가 확실히 되었다는 것을 알려주는 것이다.

여기서 IllegalStateException을 통하여 예외가 터지면 그것을 e라는 객체에 저장을 해 둡니다. 그리고 에러메세지도 검증을 해주는데, "이미 존재하는 회원입니다."라는 메세지가 일치한다면 에러가 터지지 않는데 에러가 터진다면 에러코드도 같지 않은 거겠지요.

자, 그런데 join할때 테스트를 spring이라고 똑같은 애를 자꾸 가입시키면 테스트 할때도 에러가 발생하니까 clear를 따로 진행시켜줘야 한다.
그러면 이용하고 있는 저장소를 같이 써야 하는데, test라는 저장소에

MemoryMemberRepository memberRepository = new MemoryMemberRepository();

달랑 이 코드만 작성해두면 내가 service에서 이용하고 있는 저장소와 아예 다른 새 저장소를 불러들인 것이니까 조금 찝찝하다.. 테스트는 같은 저장소를 대상으로 해야 하는데!
그래서 Service로 돌아가서 코드를 다음과 같이 갱신한다.

 private final MemberRepository memberRepository;

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

외부에서 불러와서 저장소를 생성해주는 것이다.
이제 test에 관련된 코드를 바꾸어 보도록 하자.

MemberService memberService;
    MemoryMemberRepository memberRepository;

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

service 입장에서는 외부 레포지토리를 가져와서 사용하여서 쓰는 것이니까 dependency injection 이라고 한다.
이렇게 해서 Test case까지 완벽하게 구현을 했다.

post-custom-banner

0개의 댓글