[Spring 입문] 2. 회원 관리 예제 - 백엔드

강은서·2022년 1월 8일
0

Spring

목록 보기
3/11

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

1) 일반적인 웹 어플리케이션 계층 구조

  • 컨트롤러 : 웹 MVC 컨트롤러 역할
  • 서비스 : 비즈니스 도메인 객체를 핵심 비즈니스 로직 구현
  • 레포지토리 : 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리
  • 도메인 : 비즈니스 도메인 객체, 해결하고자 하는 문제의 영역
    - 도메인 객체에 대한 비즈니스 로직이 있다고 하면, 서비스 계층에 작성하는 것 보다 해당 도메인에 작성하는 것이 좋다.
  • 데이터베이스

2) 클래스 의존 관계

  • 인터페이스로 구현 클래스를 변경할 수 있도록 설계한다.
    - 인터페이스 : 동일한 목적 하에 동일한 기능을 보장하게 위해서 이다.
  • 개발을 진행하기 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소를 사용한다.

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;
	 }
}
  • 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);
 Optional<Member> findByName(String name);
 List<Member> findAll();
}

java8에 들어간 기능으로, Optional는 null을 감싸는 wrapper클래스로 NPE(Null Pointer Exception)를 방지할 수 있도록 도와준다.

  • repository.MemoryMemberRepository
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; //key값을 생성하는 

	 @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()
				//loop를 돌면서 실행함.
			 .filter(member -> member.getName().equals(name))
			 .findAny();
	 }
	 public void clearStore() {
		 store.clear();
	 }
}
 

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

MemoryMemberRepository를 테스트하기 위한 코드

  • src/test/java/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");

		 //when
		 repository.save(member);

		 //then
		 Member result = repository.findById(member.getId()).get();
		 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);
		}
}
  • @Test annotaion을 통해서 테스트가 실행된다.
  • @AfterEach : 한번에 여러 테스트를 실행하면, 메모리 DB에 직전 테스트의 결과가 남을 수 있다. 이렇게 되면 이전 테스트 때문에 다음 테스트가 실패할 가능성이 있기 때문에 AfterEach를 사용하여 각 테스트가 종료될 때마다 메모리 DB에 저장된 데이터를 삭제한다.
    - 테스트는 각각 독립적으로 실행되어야 한다. 테스트 순서는 설계가 되지 않기 때문에 테스트 순서에 의존관계가 있는것은 좋은 테스트가 아니다.

4. 회원 서비스 개발

  • service class는 비즈니스에 의존적으로 설계하고, repository는 개발스럽게 설계한다.
  • service.MemberService
package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;

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

5. 회원 서비스 테스트

public class MemberSerivce{
	private final MemberRepository memberRepository = new MemberRepository();
}
  • service에서 만든 MemoryMemberRepository와 Test에서 만든 MemoryMemeberRepository는 다른 객체이기 때문에 문제가 발생한다.
public class MemberService{
  private final MemberRepository memberRepository;
  
  public MemberService(MemberRepository memberRepository){
  	this.memberRepository = memberRepository;
  }
}
  • 외부에서 MemberRepository를 넣어줌으로써(DI), 같은 MemberRepository를 사용한다.
  • src/test/java/MemberServiceTest
    Test는 정상 실행도 중요하지만, 예외 처리가 중요하다.(ex)중복회원예외)
package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
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);
		 IllegalStateException e = assertThrows(IllegalStateException.class,
		 () -> memberService.join(member2));//예외가 발생해야 한다.
		 assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
	 }
}

다음 글은 인프런 김영한 강사님의 스프링 강의 복습용입니다 :)

0개의 댓글