[Spring Boot] 회원 관리 예제

Jeanine·2022년 5월 30일
0

backend

목록 보기
2/3
post-thumbnail

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

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

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

  • 아직 데이터 저장소가 선정되지 않아, 인터페이스로 구현 클래스를 변경할 수 있도록 설계

2. 도메인 생성

  • 위와 같이 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;
    }
}

3. 리포지토리 생성

  • 위와 같이 repository 패키지에 MemberRepository 인터페이스와 MemoryMemberRepository 클래스 생성

1) 인터페이스

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(); // 회원 정보를 리스트로 반환
}
  • Optional의 경우에는 null 값 처리할 때 유용 (참고)

2) 구현 클래스

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

import java.util.*;

public class MemoryMemberRepository implements MemberRepository {

	// 실무에서는 동시성 문제때문에 ConcurrentHashMap, AtomicLong을 사용하는 경우가 많음
    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() {
        // 실무에서는 loop 돌리기 편하게 리스트를 많이 씀
        return new ArrayList<>(store.values());
    }

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

4. 리포지토리 테스트

  • main 메소드나 컨트롤러로 기능을 실행하면 실행하는 데 오래 걸리고, 반복 실행하기 어렵고, 여러 테스트를 한번에 실행하기 어려움
  • JUnit을 사용하자

    간단한 junit 테스트 방법 IntelliJ
    https://log-laboratory.tistory.com/203

  • 위와 같이 test 폴더에 repository 패키지를 만들고, 테스트 클래스 생성
package hello.hellospring.repository;

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

import java.util.List;

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

class MemoryMemberRepositoryTest {

    MemoryMemberRepository repository = new MemoryMemberRepository();

    @AfterEach
    public void afterEach() {
    	// 각 테스트가 실행되는 순서를 지정할 수 없으므로 순서에 대한 의존성을 없애기 위함
        // 이전 테스트의 결과가 다음 테스트에 영향을 미칠 수 있으므로 데이터 삭제 필요
        repository.clearStore();
    }

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

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

        assertThat(member).isEqualTo(result);
    }

    @Test
    public void getByName() {
        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);
    }
}

5. 서비스 개발

  • 서비스 개발 관련된 코드에서 네이밍은 최대한 '비즈니스 용어'를 사용할 것
  • 위와 같이 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 {
    private final MemberRepository memberRepository = new MemoryMemberRepository();

    public Long join(Member member) {
        // 같은 이름이 있는 중복 회원 X
        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));
    }
}
  • private final과 private static final 참고

6. 서비스 테스트

  • 서비스 클래스에 커서를 두고 Ctrl + Shift + T를 누르면 테스트 코드 자동 생성
  • 위와 같이 test 폴더에 자동으로 생성됨
  • 테스트 코드에서 함수명은 한글로 바꿔도 됨
  • 빌드할 때 테스트 코드는 포함되지 않음
  • 테스트 코드는 given, when, then 구조로 작성하면 좋음
package hello.hellospring.service;

import hello.hellospring.domain.Member;

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

import hello.hellospring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

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 join() {
        /* 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 duplicateException() {
        /* 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));
        // 두 번째 인자를 실행하여 첫 번째 인자의 예외 타입이 나오는지 확인
		
        /* then */
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}
  • 테스트 코드 작성할 때는 예외처리가 잘 되는지 확인하는 게 더 중요!
  • MemberService.java 파일을 아래와 같이 수정
    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        // 외부에서 생성
        this.memberRepository = memberRepository;
    }

참고: [인프런] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 (김영한)

profile
Grow up everyday

0개의 댓글