[인프런]스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술3

13956 단어 SpringSpring

3. 회원 관리 예제 - 백엔드 개발

3.1 비지니스 요구사항 정리

  • 데이터: 회원ID, 이름

  • 기능: 회원 등록, 조회

  • 아직 데이터 저장소가 선정되지 않음(가상의 시나리오)

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

    • 컨트롤러: 웹 MVC의 컨트롤러 역할
    • 서비스: 핵심 비지니스 로직 구현
    • 리포지토리: 도메인 객체를 데이터베이스에 저장하고 관리
    • 도메인: 비지니스 도메인 객체(예 회원, 주문, 쿠폰)
  • 클래스 의존 관계

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

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

  • 회원 객체 만들기

    • java/hello.hellospring/domain/Member.java

      package hello.hellospring.domain;
      
       public class Member {
           private Long id; // 시스템에서 설정하는 id임
           private String name;
      
           public Long getId() {
               return id;
           }
      
           public String getName() {
               return name;
           }
      
           public void setId(Long id) {
               this.id = id;
           }
      
           public void setName(String name) {
               this.name = name;
           }
       }
  • 회원 리포지토리 인터페이스 만들기

    • java/hello.hellospring/repository/MemberRepository.java
    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: null을 처리하기 위해서 사용?
        Optional<Member> findByName(String name);
        List<Member> findAll();
    }
  • 회원 리포지토리 메모리 구현체

    • java/hello.hellospring/repository/MemoryMemberRepository.java
    package hello.hellospring.repository;
    
    import hello.hellospring.domain.Member;
    
    import java.util.*;
    
    public class MemoryMemberRepository implements MemberRepository{
    
        // 동시성 문제는 생각하지 않고 단순한 구현을 위해서 HashMap()과 long을 사용함.
        // 실무에서는 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); // Map에 저장됨
            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());
        }
        
         public void clearStore() {
         store.clear();
         }
    }
  • 생성된 폴더 및 파일

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

  • 위에서 만든 회원 도메인과 리포지토리가 제대로 동작하는지 검증하기 위해서 하는 것

  • 보통 개발한 기능을 실행해서 테스트 할 때 자바의 main 메서드를 통해 실행하거나, 웹 애플리케이션의 컨트롤러를 통해서 해당 기능을 실행함

    • 단점: 준비하고 실행하는데 오래 걸림, 반복 실행 어려움, 여러 테스트 한 번에 실행하기 어려움
  • 따라서, JUnit이라는 프레임워크로 테스트를 실행함

    • 클래스 내 한 메서드만 실행하는 것이 가능하고, 클래스 레벨에서 한 클래스를 테스트하는 것도 가능하고, 전체 클래스의 테스트도 가능하다
  • 회원 리포지토리 메모리 구현체 테스트

    • src/test/java/hello.hellospring/repository/MemoryMemberRepositoryTest.java
    package hello.hellospring.repository;
    
    import hello.hellospring.domain.Member;
    import org.assertj.core.api.Assertions;
    import org.junit.jupiter.api.AfterEach;
    import org.junit.jupiter.api.Test;
    
    import java.util.List;
    
    import static org.assertj.core.api.Assertions.*;
    
    public class MemoryMemberRepositoryTest {
        MemoryMemberRepository repository = new MemoryMemberRepository();
    
        // 한 메서드의 실행일 끝날 때마다 어떤 동작을 하는 것, callback method
        @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(); // get() Option을 한 번 까서 꺼낼 수 있음.
            //System.out.println("result = "+(result == member));
            //Assertions.assertEquals(member,null);
            //Assertions.assertEquals(member,result);
            
            assertThat(member).isEqualTo(result); //Assertions.assertThat(member).isEqualTo(result);
    
        }
    
        @Test
        public void findByName(){
            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);
            //assertThat(result).isEqualTo(member2);
    
        }
    
        @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);
        }
    }
    • 결과
      • save()함수: System.out.println("result = "+(result == member));
      • save()함수: Assertions.assertEquals(member,null);
      • save()함수: Assertions.assertEquals(member,result); & assertThat(member).isEqualTo(result);
      • findByName()함수: assertThat(result).isEqualTo(member1);
      • findByName(): assertThat(result).isEqualTo(member2);
      • afterEach() 함수 없을 때 class 실행 결과
      • afterEach() 함수 있을 때 class 실행 결과
    • 테스트 경우 method 순서에 상관없이 method마다 동작 함(테스트는 각각 독립적으로 실행되어야 한다. 테스트 순서에 의존관계가 있는 것은 좋은 테스트가 아니다)
    • 이 경우, findAll()함수가 먼저 실행이 되면서 이미 "spring1", "spring2"가 저장되고, 그 후 findByName() 함수에서 get()을 하게 되면 findAll()에서 저장된 객체가 나와버리는 문제가 발생
    • 따라서, 이 문제를 해결하기 위해 mothed의 테스트가 끝나면 데이터를 clear 해줘야 하기 때문에 @AfterEach() 함수를 사용
      - @AfterEach : 한번에 여러 테스트를 실행하면 메모리 DB에 직전 테스트의 결과가 남을 수 있다. 이렇게 되면 다음 이전 테스트 때문에 다음 테스트가 실패할 가능성이 있다. @AfterEach 를 사용하면 각 테스트가 종료될 때 마다 이 기능을 실행한다. 여기서는 메모리 DB에 저장된 데이터를 삭제한다.
  • 생성된 폴더 및 파일

  • 검증을 위한 틀을 먼저 설계해서 만들어 놓는 방식을 테스트 주도 개발(TDD)이 있음

3.4 회원 서비스 개발

  • 회원 도메인과 리포지토리를 이용해서 실제 비지니스 로직을 작성하는 것

  • 회원 서비스 개발

    • java/hello.hellospring/service/MemberService.java
    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) {
    
            //Optional<Member> result= memberRepository.findByName(member.getName());
            //result.ifPresent(m -> {
            //    throw new IllegalStateException("이미 존재하는 회원입닏.");
            //});
    
            memberRepository.findByName(member.getName())
                    .ifPresent(m -> {
                        throw new IllegalStateException("이미 존재하는 회원입니다.");
                    });
        }
    
        /** 전체 회원 조회 **/
        // 서비스 클래스는 비지니스에 의존
        // 리포지토리 서비스보다 단순히 기계적으로 개발스럽게 네이밍
        public List<Member> findMember(){
            return memberRepository.findAll();
        }
    
        public Optional<Member> findOne(Long memberId){
            return memberRepository.findById(memberId);
        }
    }
    

3.5 회원 서비스 테스트

  • 클래스 이름에서 단축키 누르면 Test 클래스 틀을 만들 수 있음

    • 단축키: ctrl + shift + t
    • MemberServiceTest.java 생성
    package hello.hellospring.service;
    
    import hello.hellospring.domain.Member;
    import hello.hellospring.repository.MemberRepository;
    import hello.hellospring.repository.MemoryMemberRepository;
    import org.assertj.core.api.Assertions;
    import org.junit.jupiter.api.AfterEach;
    import org.junit.jupiter.api.BeforeEach;
    import org.junit.jupiter.api.Test;
    
    import java.util.Optional;
    
    import static org.junit.jupiter.api.Assertions.*;
    
    class MemberServiceTest {
    
        //MemberService memberService = new MemberService();
        // MemoryMemberRepository.java에 있는 객체와 같은 객체가 test 되어야 하는데 테스트 케이스에서 새로 생성했기 때문에 다른 객체를 test하게 됨.
        //MemberRepository memberRepository = = new MemoryMemberRepository();
    
        MemberService memberService;
        MemberRepository memberRepository;
    
        @BeforeEach
        public void beforeEach(){
            memberRepository = new MemoryMemberRepository();
            // member Service 입장에서 외부에서 메모리 리포지토리를 넣어줌, dependency injection
            // 같은 메모리 리포지토리를 사용하게 됨
            memberService = new MemberService(memberRepository);
        }
    
        @AfterEach
        public void afterEach(){
            memberRepository.clearStore();
        }
    
        @Test
        void 회원가입() { //테스트는 한글로 이름 적어도 상관 없음
            // given
            Member member = new Member();
            member.setName("hello");
    
            // when
            Long saveId = memberService.join(member);
    
            // then
            Member findMember = memberService.findOne(saveId).get();
            Assertions.assertThat(member.getName()).isEqualTo(findMember.getName());
    
        }
    
        @Test
        public void 중복_회원_예외(){
            //given
            Member member1 = new Member();
            member1.setName("spring"); // "hello"면 DB에 값이 누적이 되고 있기 때문에 문제가 발생함.
    
            Member member2 = new Member();
            member2.setName("spring");
    
            // when
            memberService.join(member1);
    
            IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
            Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    
            // try{
            //      memberService.join(member2);
            //      fail();
            // }catch(IllegalStateException e){
            //      Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.123123");
            // }
              //then
          }
    
          @Test
          void findMember() {
          }
    
          @Test
          void findOne() {
          }
      }
    • 추가로 MemoryService.java 수정
     // 기존 코드: 회원 서비스가 메모리 회원 리포지토리를 직접 생성함
     private final MemberRepository memberRepository = new MemoryMemberRepository();
    
      // 수정 코드: 회원 메모리 리포지토리 코드가 회원 서비스 코드를 DI 가능하게 변경함
      private final MemberRepository memberRepository;
    
      // 외부에서 넣어주는 걸로 바꿈
      public MemberService(MemberRepository memberRepository){
          this.memberRepository = memberRepository;
      }
    • @BeforeEach: 각 테스크 실행전에 호출된다, 테스크가 서로 영향이 없도록 항상 새로운 객체를 생성하고 의존관계도 새로 맺어준다.

참고문헌

[인프런] 김영한 강사님, 「스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술」

좋은 웹페이지 즐겨찾기