본문 바로가기
개발 관련 공부/스프링 김영한 로드맵

[스프링 입문] 섹션 3. 회원 관리 예제 - 백엔드 개발

by 슴새 2022. 9. 18.
반응형

비지니스 요구사항 정리

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

컨트롤러: 웹 MVC의 컨트롤러 역할

서비스: 핵심 비즈니스 로직 구현

리포지토리: 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리

도메인: 비즈니스 도메인 객체, 예) 회원, 주문, 쿠폰 등등 주로 데이터베이스에 저장하고 관리됨

아직 db를 뭐쓸지 결정하지 않았으므로, 일단 인터페이스 기반으로 개발한다.

 

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

이렇게 만들어준다.

package hello.hellospring.domain;

public class Member {
    private Long id;
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }
}
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);
    Optional<Member> findByName(String name);
    List<Member> findAll();
}
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.*;
/**
 * 동시성 문제가 고려되어 있지 않음, 실무에서는 ConcurrentHashMap, AtomicLong 사용 고려
 */
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 List<Member> findAll() {
        return new ArrayList<>(store.values());
    }
    @Override
    public Optional<Member> findByName(String name) {
        return store.values().stream() //하나 찾으면 걔를 반환
                .filter(member -> member.getName().equals(name))
                .findAny();
    }
    public void clearStore() {
        store.clear();
    }
}

코드는 이렇다...

Optional 같은 생소한 것들도 있지만 대부분 익숙한 구조와 흐름이다.

 

null이 반환될 가능성이 있는건 Optional.ofNullable로 감싸준다. 그러면 클라에서 널 관련 처리를 해줄 수 있다.

 

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

테스트 케이스 작성에는 JUnit이라는 프레임워크를 이용한다.

테스트 쪽에 작성하자.

MemoryMemberRepository의 테스트를 작성하는거니 MemoryMemberRepositoryTest라고 이름붙였다.

(보통 클래스명+Test로 명명하는게 관례)

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();
    }
    //아까 만들었던 save가 잘 동작하는지 체크하는 테스트
    @Test
    public void save() {
        //given
        Member member = new Member();
        member.setName("spring");
        //when
        repository.save(member);
        //then
        //저장을 해 본 다음에, 그걸 꺼내본다.
        Member result = repository.findById(member.getId()).get();
        //result==member인지 체크하는 함수
        assertThat(result).isEqualTo(member);
    }
    //다른 함수들도 마찬가지로..
    @Test
    public void findByName() {
        //given
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);
        Member member2 = new Member();
        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);
    }
}

이렇게 작성하고 실행하면, 각각의 테스트 함수들이 같이 실행된다.

 

대부분의 테스트들은 given when then으로 구현부를 나눌 수 있다.

이런 상황에, 이거를 실행했을때, 이렇게 나와야 

..라는 뜻이다.

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

처음에 있는 이것은 이전 테스트의 영향으로 뭔가 이상해지지 않도록 청소해주는 것이다.

 

+)테스트는 각각 독립적으로 실행되어야 한다. 테스트 순서에 의존관계가 있는 것은 좋은 테스트가 아니다.

+)테스트를 먼저 만들고 거기에 맞춰서 클래스를 짜는게 TDD이다. 

회원 서비스 개발

실제 비지니스 로직을 작성하는 부분이다.

'인터페이스를 구현한 MemoryMemberRepository'의 객체를 선언해서 지지고 볶는 클래스랄까...

역시 자바를 배우면서 음악관리자..책 관리자...등등 클래식한 예제를 만들어보면서 많이 본 익숙한 구조이다.

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

이렇게 만들어준다.

회원 서비스 테스트

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

일단 MemberService에서 객체 생성 부분을 다음과 같이 변경한다. 

 

그리고 테스트를 작성할건데..

테스트 만들때 팁이 있다.

ctrl+shift+T 누르면

이렇게 팝업이 뜨고 더블클릭하면

팝업이 뜨는데 테스트하고 싶은 함수들 다 체크하고 오케이 

자동으로 껍데기가 완성된다.

이 테스트코드들은 한글로 막 적어도 된다. 직관적으로 알아볼 수 있으니까.

 

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;
    MemoryMemberRepository memberRepository;
 
    @BeforeEach
    public void beforeEach() {
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository);
    }
    @AfterEach
    public void afterEach() {
        memberRepository.clearStore();
    }
    @Test
    public void 회원가입() throws Exception {
        //Given
        Member member = new Member();
        member.setName("hello");
        //When
        Long saveId = memberService.join(member);
        //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");
        //When
        memberService.join(member1);
        //assertThrows= 이런상황에서 이런 에러가 터져야돼..
        IllegalStateException e = assertThrows(IllegalStateException.class,
                () -> memberService.join(member2));//예외가 발생해야 한다.
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }
}

이번 테스트 코드는 예외적인 경우의 테스트도 작성한다. 유의해서 보기

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

처음에 있는 이거는 테스트가 서로 영향이 없도록 항상 새로운 객체를 생성하고,의존관계도 새로 맺어주는 것이다.

테스트 실행 전에 호출된다.

이 강의는 인프런 김영한님의 스프링 입문 강의를 듣고 정리한 것입니다.
강의 링크: https://url.kr/by9hmf
반응형

댓글