회원 관리 예제(백엔드 개발)

KyungHoon Kim·2022년 9월 21일
0

스프링

목록 보기
3/6

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

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

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

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

클래스 의존관계

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


2. 회원 도메인과 리포지토리 생성

회원객체

src/main/java/domain(패키지)/Member(클래스)

package hello.hellospring.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;
    }
}

회원 리포지토리 인터페이스

src/main/java/repository(패키지)/MemberRepository(인터페이스)

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

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

public interface MemberRepository {
    Member save(Member member); // 저장소에 회원이 저장되는 기능
    Optional<Member> findById(Long id);
    // findBy로 가져올 때 null일 수 있는데, 그런 경우를 대비해서 Optional을 사용한다.  
    Optional<Member> findByName(String name);
    List<Member> findAll(); // 저장된 모든 회원 리스트를 띄워준다
}

회원 리포지토리 메모리 구현체

src/main/java/repository(패키지)/MemoryMemberRepository(클래스)

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<>();
    // 메모리 저장을 어딘가에 해놔야된다. 
    // 회원id는 키(key)는 long, 값(value)은 member로 map으로 받는다.
    private static long sequence = 0L; 
    // 시퀀스는 0,1,2 .. key값을 생성해주는 것
    /**
     * 동시성 문제가 고려되어 있지 않음, 실무에서는 ConcurrentHashMap, AtomicLong 사용 고려
     */

    @Override
    public Member save(Member member) {
        member.setId(++sequence);
       // setId 할 때 멤버를 저장할 때 시퀀스 값을 하나 올려준다.
       // Id는 고객이 정하는게 아니라 시스템에서 시퀀스를 올려주는 것으로 세팅
        store.put(member.getId(), member); 
      // 그 다음, store에 Id를 넣으면 map에 저장된다.
        return member;
    }
    @Override
    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }
    // null이 반환될 가능성이 있으면, Optional.ofNullable로 감싸면 된다. 

    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());
        // store에 있는 values는 Member들이다.(map에서 value값)
    }
    @Override
    public Optional<Member> findByName(String name) {
        return store.values().stream()
                .filter(member -> member.getName().equals(name))
     	// getName과 파라미터로 넘어온 name이랑 같은 경우에만 필터링이 된다.
                .findAny(); 
        // 루프를 돌면서 하나 찾아지면 반환, 끝까지 돌고 없으면 Optional에 null이 포함되서 반환된다.
    }
    public void clearStore() {
        store.clear();
    }
}

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

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

회원 리포지토리 메모리 구현체 테스트

• src/test/java 하위 폴더에 테스트 클래스 생성한다.
• 테스트를 돌리기 전 먼저 main 메소드의 run을 중지시킨다
• 클래스단위로 테스트를 돌릴 수도 있고, 각 테스트별로 테스트를 돌릴 수도 있다.
• @AfterEach: 한번에 여러 테스트를 실행하면 메모리 DB에 직전 테스트의 결과가 남을 수 있다. 이렇게 되면 다음 이전 테스트 때문에 다음 테스트가 실패할 가능성이 있다. @AfterEach 를 사용하면 각 테스트가 종료될 때 마다 이 기능을 실행한다. 여기서는 메모리 DB에 저장된 데이터를 삭제한다.
• 테스트는 각각 독립적으로 실행되어야 하고, 테스트 순서에 의존관계가 있는 것은 좋은 테스트가 아니다.

src/test/java/hello.hellospring/repository/MemoryMemberRepositoryTest

package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;

class MemoryMemberRepositoryTest {
    MemoryMemberRepository repository = new MemoryMemberRepository();

    @AfterEach
    /* 각 테스트가 끝날 때마다 하나의 동작을 넣는 것이다.
    * 각 테스트가 끝날 때마다 클리어 해주지 않으면,
    * 테스트 순서가 제 각기라서 데이터가 중복되는 등 테스트가 정상작동 하지 않을 수 있다.
    * 테스트는 순서에 의존적으로 작성하면 안되고, 순서에 무관하게, 의존관계가 없이 설계가되어야 한다.
    * */
    public void afterEach() {
        repository.clearStore(); 
        // 테스트가 실행되고 끝날 때마다 저장소를 한번씩 지운다.
    }
    @Test
    public void save() {
        // given
        Member member = new Member();
        member.setName("spring"); //member에 name을 spring으로 세팅
        // when
        repository.save(member); // repository에 저장
        // then
        Member result = repository.findById(member.getId()).get();
        // 반환타입이 optional, 좋은 방법은 아니나 테스트 코드에서는 괜찮다.
        // optional을 get으로 꺼내서 result라고 한 후 아래에서 검증
        assertThat(result).isEqualTo(member);
        // Assertions org.assertj.core.api 선택하면 assertThat사용 가능 
        // import로 junit에 있는 Assertions가 아닌지 확인할 것
        // member가 result와 동일한지 확인하는 것이다. 동일하면 녹색불, 실패하면 x표시가 뜬다
    }
    @Test
    public void findByName() {
        // given
        Member member1 = new Member();
        member1.setName("spring1"); 
        // member1에 이름을 spring1로 설정
        repository.save(member1);

        Member member2 = new Member(); 
        // shift+F6키를 통해 member1을 member2로 간단히 바꿀 수 있다.
        member2.setName("spring2");
        repository.save(member2);

        // when
        Member result = repository.findByName("spring1").get();
        // then
        assertThat(result).isEqualTo(member1);
    }
    @Test
    public void findAll() {
        // given
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);

        Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2);
        // when
        List<Member> result = repository.findAll();
        // then
        assertThat(result.size()).isEqualTo(2);
    }
}

4. 회원 서비스 개발

src/main/java/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;
// 아래 MemberService를 드래그하고 Ctrl+Shift+T를 누르면 테스트를 바로 만들 수 있다.

public class MemberService { 
// 서비스의 역할이 비즈니스 로직을 처리하기 때문에 
// 그 역할에 맡게 비즈니스에 가까운 네이밍을 한다(레포지토리와 달리)
    private final MemberRepository memberRepository;
    // 회원서비스를 만드려면 먼저 회원 레포지토리가 있어야겠지?
    public MemberService(MemberRepository memberRepository){
        this.memberRepository = memberRepository;
    /*
    MemberService 입장에서 MemberRepository를 new로 생성하는 것이 아니라, 외부에서 넣어주는 것을 DI(의존성 주입)라고 한다.
    */
    // 그러면, MemberServiceTest 24라인을 통해 memberRepository가 들어오고,
    // 결과적으로 MemberServiceTest 18라인의 memberRepository(23라인에서 MemoryMemberRepository를 넣었기 때문에)와
    // MemberService의 12라인에서는 같은 MemoryMemberRepository를 사용하게 된다
    }

    // 회원가입
    public Long join(Member member) {
        validateDuplicateMember(member); // 중복 회원 검증을 위한 함수를 밑에서 생성해서 사용
        memberRepository.save(member);
        return member.getId(); // 임의로 Id를 반환
    }
    
    private void validateDuplicateMember(Member member) { 
 // cf) 커서를 member) 뒤에 두고 ctrl+alt v를 하면 바로 리턴을 받을 수 있다.
        memberRepository.findByName(member.getName()) 
 // findByName을 하면 결과가 Optional멤버니깐 바로 ifPresent를 한다.
                .ifPresent(m -> {  // m은 member를 가리킨다.  
                // ifPresent의 역할 : null이 아니라 어떤 값이라도 있으면 작동한다(Optional로 감쌌기 때문에 가능)
                    throw new IllegalStateException("이미 존재하는 회원입니다."); 
                // 중복이면 예외발생하게 설정
                });
    }

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

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

5. 회원 서비스 테스트


src/test/java/hello.hellospring/service/MemberServiceTest

package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
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.*;
import static org.junit.jupiter.api.Assertions.*;
class MemberServiceTest {
    /*
    MemberService memberService = new MemberService();
    MemoryMemberRepository memberRepository = new MemoryMemberRepository();
    
    // 여기서 new로 만든 MemoryMemberRepository()와 MemberService에서 쓰는 
    // MemoryMemberRepository()는 서로 다른 리포지토리다.
    // 같은 리포지토리로 테스트를 하는 것이 올바른 방법이기 때문에
    // 같은 인스턴스를 사용하도록 바꿔줘야 한다.
     */

    MemberService memberService;
    MemoryMemberRepository memberRepository; // AfterEach에서 memberRepository를 쓰려면 가져와야 된다

    @BeforeEach
    public void beforeEach() {
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository);
        // 각 테스트가 실행하기 전에 MemoryMemberRepository를 만들고나서,
        // 19라인에 있는 memberRepository에 넣어놓고, 24라인의 MemberService에 memberRepository를 넣는다.
        // 그러면 MemberService.java의 12라인에서 여기 23라인에서 생성된 MemoryMemberRepository가 들어간 memberRepository를 사용하므로
        // 동일한 MemoryMemberRepository를 사용하게 된다.
    }
    @AfterEach
    public void afterEach() {
        memberRepository.clearStore();
    }


    @Test
    public void 회원가입() throws Exception {
        // Given
        Member member = new Member();
        member.setName("hello");
        // When // saveId는 리턴이 저장한 Id가 튀어나오기로 함
        Long saveId = memberService.join(member); //memberService의 join을 검증
        // Then
        Member findMember = memberRepository.findById(saveId).get();
        // 우리가 저장한게 레포지토리에 있는게 맞아?를 확인
        assertEquals(member.getName(), findMember.getName());
    }
    @Test
    public void 중복_회원_예외() throws Exception {
        // Given
        Member member1 = new Member();
        member1.setName("spring");
        Member member2 = new Member();
        member2.setName("spring");  //spring으로 중복되게 설정
        // When
        memberService.join(member1);

        /* try-catch로 예외문에 대한 테스트 코드를 작성할 수는 있으나, 문법으로 처리하는게 더
        try {
            memberService.join(member2);
            fail();
        } catch (IllegalStateException e){
            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
        }
        */
        IllegalStateException e = assertThrows(IllegalStateException.class,
                () -> memberService.join(member2));
                // member2를 조인하면 예외가 발생해야 한다. 그 때 발생해야되는 에러는 IllegalStateException
        // 아래 코드에서 반환되는 메시지를 검증
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다."); 
        // 중복일 때 출력되는 메시지가 왼쪽에 기대값과 같은지 확인
    }
}
profile
주니어개발자 시켜줘요

0개의 댓글