[spring] #8 스프링 입문

장재욱·2022년 5월 14일
0

Spring boot 입문

목록 보기
8/8
post-thumbnail

이번 글에서는 회원 도메인, 레포지토리, 서비스들과 각각의 테스트 케이스 까지 작성해 보는 강의를 듣고 복습 해보겠습니다.

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

hello1.hellospring1.domain package를 생성 후 Member class 생성

package hello1.hellospring1.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;
    }
}
  • hellospring1.domain.Member

또 hellospring1.respository.MemberRepository interface 생성

package hello1.hellospring1.repository;

import hello1.hellospring1.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);
    // Optional은 null값이 나올 때 null을 반환하는 것이 아닌 optional로 감싸서 반환
    List<Member> findAll();
}
  • hellospring1.repository.MemberRepsoitory

구현체인 class파일 생성 MemoryMemberRepository 생성

package hello1.hellospring1.repository;

import hello1.hellospring1.domain.Member;

import java.util.*;

public class MemoryMemberRepository implements MemberRepository {
// option + enter 키로 import 해줌

    private static Map<Long, Member> store = new HashMap<>();
    // 실무에서는 동시성 문제가 있을 수 있기 때문에 공유되는 변수 일 때는 
    // 컨커런시 해시맵을 사용해야 하지만 예제이니 해시맵 사용
    private static long sequence = 0L;
    // 여기서도 원래는 automicLong 사용해야 함

    @Override
    public Member save(Member member) {
        member.setId(++sequence);
        store.put(member.getId(), member);

        return  member;
    }
    // store에 넣기 전에 member에 아이디값을 세팅해주고 이름은 넘어온 상태를 저장

    @Override
    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }
    // 결과가 없을 때는 null이 발생하는데 
    // ofNullable을 사용하면 null값이어도 Optional로 감싸서 반환할 수 있다.
    // 이렇게 감싸서 반환해 주면 클라이언트에서 사용할 수 있는 기능이 있다.

    @Override
    public Optional<Member> findByName(String name) {
        return store.values().stream()
                .filter(member -> member.getName().equals(name))
                .findAny();
    }
    // lamda 표현식으로 구현 
    // member의 name값이 parameter로 넘어온 name과 같은지 확인하고
    // 같은 경우에는 필터링이 되고 찾으면 반환이 된다.

    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }
    // 자바에서 실무할 때는 List를 많이 사용한다.
    // store.values()는 member들이다.

    public void clearStore() {
        store.clear(); // store를 비워줌
    }
}
  • hellospring1.repository.MemoryMemberRepository

이 MemoryMemberRepository를 만들고 이게 정상적으로 동작하는지 확인하기 위해 테스트 케이스를 사용한다.

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

테스트 케이스 작성

개발한 기능을 실행해서 테스트 할 때 자바의 main 메서드를 통해서 실행하거나, 웹 애플리케이션의 컨트롤러를 통해서 해당 기능을 실행한다. 이러한 방법은 준비하고 실행하는데 오래 걸리고, 반복 실행하기 어렵고 여러 테스트를 한번에 실행하기 어렵다는 단점이 있다. 자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결한다.

main쪽이 아닌 test쪽에서 똑같은 package 생성 후 클래스파일을 생성한다.

package hello1.hellospring1.repository;

import hello1.hellospring1.domain.Member;
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();

    @AfterEach
    public void afterEach() {
        // afterEach 없이 전체를 Test하면 findByName이 오류가 난다.
        // 테스트 순서가 findAll이 먼저 실행되고 findByName이 실행되면서 
        // spring1과 spring2가 이미 저장되었기 때문에 오류가 난다.
        // 그러므로 테스트가 하나 끝날 때 마다 데이터를 clear 해주어야 한다.
        // 그 기능을 하는 것이 afterEach 이다.

        repository.clearStore(); 
        // MemoryMemberRepository에 clearStore를 통해 데이터를 clear해주는 메소드 생성
    }

    @Test
    public void save() {
        Member member = new Member();
        member.setName("spring"); // 이름 세팅

        repository.save(member);

        Member result = repository.findById(member.getId()).get(); 
        // 검증, 반환타입 Optional이면 get을 통해 꺼낼 수 있음
        assertThat(member).isEqualTo(result); 
        // Assertions 기능 사용 result와 member의 값이 같은지 확인
    }

    @Test
    public void findByName() {
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);

        Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2); 
        // member1 > member2로 바꿀 때 shift + f6 사용시 한번에 바꿀 수 있음

        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); // 결과가 2개 인지?

    }
}
  • hellospring1.repository.MemoryMemberRepositoryTest

지금까지 우리가 한 것은 repository를 만들고 test를 만들어서 테스트했지만
이를 반대로 테스트를 먼저 만들고 repository(구현 클래스)를 작성하는 방법을 TDD (테스트 주도 개발 - Test-Driven Development)라고 한다.

회원 서비스 개발

sevice package 생성 후 MemberService class 생성

package hello1.hellospring1.service;

import hello1.hellospring1.domain.Member;
import hello1.hellospring1.repository.MemberRepository;

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

public class MemberService {
    // Test 쉽게 만들기 (단축키 command + shift + t 후 test 할 메소드 선택 후 ok)

    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    } 
    // constructor 사용 || MemberRepository를 new로 만드는게 아닌 외부에서 넣어주도록 만들어준다.

    // 회원가입
    public Long join(Member member) {
        validateDuplicateMember(member); 
        // 중복 회원 검증 : 같은 이름의 중복 회원이 있으면 안된다.
        memberRepository.save(member);

        return member.getId();
    }

    // 같은 이름이 있는 중복 회원 X 메소드
    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                        .ifPresent(m -> {
                            throw new IllegalStateException("이미 존재하는 회원입니다.");
                        }); 
                        // 메소드로 뽑기 (단축키 ctrl + t 에서 extract Method || option + command + M)
                        // IllegalStateException : 불법 또는 부적절한시기에 메소드가 호출되었음을 알립니다.

        // Optional<Member> result = memberRepository.findByName(member.getName());
        // result.ifPresent(m -> {
        //     throw new IllegalStateException("이미 존재하는 회원입니다.");
        // }); 코드를 더 간단하게 만들기
    }

    // 전체 회원 조회
    public List<Member> findMembers() {

        return memberRepository.findAll();
    }

    // 회원 조회
    public Optional<Member> findOne(Long memberId) {

        return memberRepository.findById(memberId);
    }
}
  • hellospring1.service.MemberService

회원 서비스 테스트

위의 주석에서 볼 수 있듯이 public class MemberService 에서
commnad + shift + t 를 통해 쉽게 테스트 케이스를 만들 수 있다.

package hello1.hellospring1.service;

import hello1.hellospring1.domain.Member;
import hello1.hellospring1.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에서 사용하는 new MemberMemoryRepository와
    // test에서 사용하는 new MemberMemoryRepository가 다르다. 서로 다른 걸로 테스트를 하고 있으니 아래와 같이 바꾼다.

    MemberService memberService;
    MemoryMemberRepository memberRepository;

    @BeforeEach
    public void beforeEach() {
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository);
    }
    // 테스트 실행할 때 마다(실행 하기 전에) 독립적으로 생성
    // MemoryMemberRepository를 만들어서 memberRepository에 넣어주고
    // memberRepository를 MemberService에 넣어주면 같은 MemoryMemberRepository를 사용하게 된다.
    // 이런 것을 DI(dependency injection) 의존성 주입이라 한다.

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

    @Test
    void 회원가입() { // given when then 패턴 사용
        // given 이 데이터를 기반
        Member member = new Member();
        member.setName("hello");

        // when 이걸 검증
        Long saveId = memberService.join(member);

        // then 검증 부분
        Member findMember = memberService.findOne(saveId).get(); 
        // 이 데이터가 repository에 있는게 맞는지?
        assertThat(member.getName()).isEqualTo(findMember.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("이미 존재하는 회원입니다.");

        /*
        memberService.join(member1);

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

        // then
    }

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}
  • MemberServiceTest

다음 시간에 DI 의존성 주입에 대하여 더 알아볼 예정이다.

profile
아기개발자

0개의 댓글