WEB/Spring

김영한 (스트링부트 입문) 2

Tony Lim 2021. 1. 24. 19:52
728x90
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;
    }
}

 

도메인 설정을 해줍니다

 

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

 

추후에 구현체가 jdbctemplate 이나 mybatis등등으로 바뀔수 있기에 인터페이스 설정을 해줍니다. findbyid, findbyName 할떄 null이 올수도 있음으로 Optional 로 처리해줍니다.

 

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

import java.util.*;

public class MemoryMeberRepository implements MemberRepository
{
    private static Map<Long, Member> store = new HashMap<>(); // ConcurrentHashmap 을 써야한다 공유되는 자원이기에 동시성 문제가 생길 수 있다.
    private static long sequence = 0L;  // AtomicLong을 써줘야한다 실무에서는 동시성 문제


    @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()
    {
        return new ArrayList<>(store.values());
    }
    
    public void clearStore()
    {
        store.clear();
    }

}

 

동시성 문제를 해결하기위한 방법은 주석으로 달아놓았다. findbyId는 앞에 언급한것처럼 null 이올수있기에 Optional.ofNullable로 처리해줍니다.

findByName 은 hashmap에 있는 value 들을 iterate 하면서 그중 이름이 같은것을 찾는 즉시 그 member 객체를 return 해줍니다.

Test case 작성

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.*;

class MemoryMemberRepositoryTest
{
    MemoryMeberRepository repository = new MemoryMeberRepository();

    @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 findByName()
    {
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);

        Member member2 = new Member();
        member2.setName("spring1");
        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("spring1");
        repository.save(member2);

        List<Member> result = repository.findAll();

        assertThat(result.size()).isEqualTo(2);
    }

}

 

실무에서는 build할떄 test를 통과하지 못하면 다음 단계를 못넘어가게 막아버린다.

rename을 전체적으로 하고싶으면 해당 variable 에서 shift F6을 누르면 된다.

전체 test를 돌릴떄 순서가 보장이안되기에 에러가 생길 수 있다. 예를들면 findAll이 먼저 실행이 되고 findByName이 실행이되면 findAll에서 저장된 객체는 findByName에서 생성한 객체와 다르므로 에러를 가져온다. 항상 test가 끝난 메소드는 클리어를 해주어야한다.

@AfterEach 는 매번 테스트 메소드가 실행이 끝나면 불려지는 콜백함수를 정의 할 수 있다 이안에 공용저장소를 clear해주는 코드를 넣어주면 된다.

테스트 먼저 작성하고 실제 코드를 작성하는것!! 틀을 먼저 만드는 것!! Test-driven development. Test는 정말 중요하다 하셨다

 

Business Logic 1 (Service)

package hello.hellospring.service;

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

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

public class MemberService
{
    
    private final MemberRepository memberRepository;

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

    /**
     * 회원 가입
     */
    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);
    }
}

 

Repository같은 경우는 단순한 데이터 베이스와 CRUD 를 담당해주지만 Service 클래스는 이름만 봐도 약간 비즈니스 로직같은 다른 것을 담당하는 것을 알 수 있다.

findByName이 어차피 Optional 클래스로 감싸서 리턴 하기떄문에 ifPresent를 통해 람다 형식으로 이름이 같으면 Exception을 발생시키게 해줌으로 이름이 같은 사람들은 가입 못하게 하였다.

memberRepository 를 주입받음 으로서 추후에 테스트할때 같은 memberRepository 객체를 쓸 수 있다. 이런 방식을 Dependency Injection 이라 한다.

 

Service Test 

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMeberRepository;
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;
    MemoryMeberRepository memoryMeberRepository; 

    @BeforeEach
    public void beforeEach()
    {
        memoryMeberRepository = new MemoryMeberRepository();
        memberService = new MemberService(memoryMeberRepository);
    }


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

    @Test
    void 회원가입()
    {
        //given
        Member member = new Member();
        member.setName("spring");

        //when
        Long saveId = memberService.join(member);

        //then
        Member findMemeber = memberService.findOne(saveId).get();
        assertThat(member.getName()).isEqualTo(findMemeber.getName());
    }

    @Test
    public void 중복_회원_예외()
    {
        //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("이미 존재하는 회원입니다.");

//        try
//        {
//            memberService.join(member2);
//            fail();
//        }
//        catch(IllegalStateException e)
//        {
//            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
//        }

        //then
    }




    @Test
    void findMembers()
    {
    }

    @Test
    void findOne()
    {
    }
}

 

BeforeEach 로 저렇게 한 이유는 같은 MemoryMemberRepository 를 쓰기 위함이다. 

로직상 Repository를 넣어주고 클리어하고를 반복한다. 또한 테스트 할때는 한국말로 메소드를 명시해도 좋다.

728x90

'WEB > Spring' 카테고리의 다른 글

김영한 (스프링 부트 5)  (0) 2021.02.08
김영한 (스프링부트 입문) 4  (0) 2021.02.08
김영한 (스프링부트 입문) 3  (1) 2021.01.31
김영한 (스프링부트 입문) 1  (0) 2021.01.24
Spring Ehcache example  (0) 2021.01.23