인프런/스프링 입문

4)비즈니스 요구사항 정리,회원도메인,회원 리포지토리생성,테스트코드생성,테스트해보기

backend dev 2022. 10. 12.

강의에서 진행하는 회원 서비스 개발 따라하기

데이터: 회원id, 이름

기능 : 회원 등록,조회

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

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

 

 

 

 

클래스 의존관계

데이터 저장소가 선정되지않아서 멤버저장소는 인터페이스로 구현한다.

데이터저장소는 구현체를 메모리 구현체로 만들것임. 향후에 RDB든 뭐든 정해지면 바꿔낄것임

바꿀거기 때문에 인터페이스로 정의해두었다. 

 

 

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

 

회원도메인(패키지폴더)를 만들어주고 그안에 Member를 만들어준다.

 

멤버.java

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

다음과 같이 멤버를 구성해준다 ( 위에서 데이터로 사용할 id와 이름을 넣었음 + getter,setter)

 

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

다음과 같이 멤버를 저장하고 저장한 멤버를 돌려주는 기능

아이디를 이용하여 멤버를 찾는기능

이름을 이용하여 멤버를 찾는기능

모든 멤버를 조회하는 기능을 

가지는 멤버리포지토리 인터페이스를 만든다.

 

팁 -> 컨트롤+ 쉬프트 + 엔터로 커서가 괄호사이 안쪽에있어서 해당 단축키로 맨끝에 세미클론을 붙이며 빠져나오기가능

 

 

 

MemoryMemberRepository.java 구현

위에서 작성한 메모리레포지토리 인터페이스를 구현하여 

메모리 멤버 레포지토리를 구현한다. 

가상의 저장공간을 Map으로서 구현한것이다.

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

import java.util.*;

public class MemoryMemberRepository implements MemberRepository {
    //실무에서는 동시성문제가있을수있어서 공유되는 변수일때는 다른걸 써야하는데 여기는 예제이므로 Map으로 진행
    //저장할 변수를 선언해준다.
    private static Map<Long,Member> store = new HashMap<>(); //가상의 저장공간으로 Map을 사용
    private static long sequence = 0L; //0,1,2처럼 키값을 생성해주는 역할


    @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)); //id를 이용하여 Map에서는 키값이니까 키값에 해당한 밸류를 가져온다.
        //하지만 값이 없을경우에 null이 반환될텐데 null이 반환되는것을 막기위해 Optional.ofNullable()이라는것으로 감싸준다.
        //감싸서 보내주면 클라이언트에서 뭔가 할 수 있음
    }

    @Override
    public Optional<Member> findByName(String name) {
        //.values()는 해당 map의 value 목록을 Collection 형태로 리턴한다. 여기서는 Collection<Member> 형태로 반환.
        return store.values().stream() // 밸류값 콜렉션 객체로 스트림객체를 만들어주고
                .filter(member -> member.getName().equals(name)) // 스트림객체에서 입력으로 들어온 이름과 같은 멤버 네임을 가진애들만 뽑아서 구성해주고
                .findAny(); //그 중 아무거나 하나를 고른다, 그 후 return이므로 그걸 반환해줌 , 없다면 null 하지만 optional로 감싸져서 리턴된다.

    }

    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }
}

다음과 같고 각 설명은 주석으로 달아보았다.

Stream과 Optional에 관하여는 더 공부가 필요하다.

 

스트림관련 참고.

 

[Java] 자바 스트림(Stream) 사용법 및 예제

자바 스트림(Stream) 자바의 스트림(Stream)은 'Java 8'부터 지원되기 시작한 기능이다. 컬렉션에 저장되어 있는 엘리먼트들을 하나씩 순회하면서 처리할 수 있는 코드패턴이다. 람다식과 함께 사용되

hbase.tistory.com

optional 참고

 

[Java] Optional 사용법 및 예제

자바 8부터 Optional이라는 클래스가 지원되기 시작했다. Optional 클래스란 어떤 목적으로 사용되는지 알아보자. Java NPE 예방 자바 프로그램 코드를 작성하다보면 null 값에 대해 고려해야하는 경우

hbase.tistory.com

 

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

 

개발한 기능을 실행해서 테스트 할 때 자바의 main 메서드를 통해서 실행하거나, 

웹 애플리케이션의 컨트롤러를 통해서 해당 기능을 실행한다. 

이러한 방법은 준비하고 실행하는데 오래 걸리고, 반복 실행하기 어렵고 여러 테스트를 한번에 실행하기 어렵다는 단점이 있다. 

자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결한다.

 

프로젝트상황

프로젝트에 원래 main과 test라는 폴더로 나누어져있다. 

test안에도 java -> hello.hellospring 처럼 구성이 되어있었고

그안에 repository 디렉토리를 만들고 그안에 메모리멤버 리포지토리를 테스트하기위한

MemoryMemberRepositoryTest 를 만들어준다. 이름뒤에 Test를 붙여준다.

 

메소드 위에 @Test 어노테이션을 붙여주면 해당 메소드만 돌릴수있게 옆에 아이콘이 나온다. 신기하다.

 

클래스 이름옆에 , 메소드 이름옆에 뭔가 생긴걸 볼수있다. 메소드 각각 또는 클래스 전체를 실행해볼수 있음.

 

 

 

Assertions중 어떤걸 사용하는가?

두번째인 org.assertj.core.api를 사용한다.

Assertions를 매번치기 힘들때 사용하는 방법

Assertions를 클릭후 알트+엔터하고 static import를 선택해준다.

Assertions이 블럭처리 되게끔 클릭 후 알트 + 엔터 하고 static import
위에 해당 static import가 생기고
원래 메소드가 이런식으로 줄어들어있다. -> 다음부터는 assert~ 부터 사용하면됨

전체적인 코드모습 (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 java.util.Optional;

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

class MemoryMemberRepositoryTest { //다른곳에 가져다 쓸게아니니까 public일 필요가없음

    MemoryMemberRepository repository = new MemoryMemberRepository();

    @AfterEach //하나의 테스트 메소드가 동작이 끝날때마다 해당 어노테이션이 붙은 메소드를 동작하라는뜻.
    public void afterEach()
    {
        repository.clearStore(); // 저장소에있는 내용 삭제
    }



    @Test
    public void save(){
        Member member = new Member(); //멤버 객체를 만들고
        member.setName("spring"); // 이름설정해주고

        repository.save(member); // 만든 멤버를 저장해보고

        //잘 저장됬는지 id를 리턴받아본다. 반환값이 Optional<Member>인데 .get()으로 Optional안에있는값을 꺼내볼수있다.
        //.get()말고 다른게쓰이긴하는데 테스트에서는 그냥 쓰인다고함.
        Member result = repository.findById(member.getId()).get();

        //검증방법 1. 내가 만든 member와 , result로 받은 멤버가 같은지 비교
//        System.out.println("result = " + (result == member));

        //하지만 콘솔에 찍어서 눈으로 확인하는 방법보다는 Assertions.assertThat(member).isEqualTo(result)를 많이쓴다.
        assertThat(member).isEqualTo(result); //실제값 예상값 순으로 넣는다. 
        //돌려보면 출력되는건 없지만 잘돌아간다면 Run 왼쪽아래에 녹색체크모양이 뜬다.

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

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

        Member member2 = new Member(); //shift+F6으로 편하게 수정해주고
        member2.setName("spring2");
        repository.save(member2);

        List<Member> result = repository.findAll(); //컨트롤+알트+v로 편하게 변수 생성해준다.

        assertThat(result.size()).isEqualTo(2); // 갯수로 테스트해본다.
    }





}

 

 

테스트 결과 모습

통과 됬을때 녹색체크가 뜬다! // 콘솔에 찍힌건 주석처리안해서.
실제값을 null로 바꾸니 기대한값과 다르다고 오류를 발생해주는 모습

Tip -> 같은걸 복사했을때 쉽게 이름 바꾸는법

멤버 관련 3줄의 코드를 다 복사해서 밑에멤버는 멤버2로 바꾸고싶을때 멤버1 앞에서 Shift + F6 을 눌러주고 수정하면 나머지 초록색 칸되어있는애들도 같이 바뀐다.

컨트롤  + p  (어떤값을 넣어야하는지)

컨트롤+쉬프트+p (어떤값이 리턴되는지) 확인할 수 있다.

 

컨트롤 + 알트 + v  -> 변수 추출하기 (새로운 변수로 선언)

단축키 치기 전
단축키 치고 난후

다음과같이 리턴값을 받아줄 변수가 자동으로 생성된다. 수정하고 싶은거 수정하고 엔터치면 확정됨.

 

컨트롤 + 쉬프트 + f10으로 커서가있는 위치의 메소드를 실행해주고

쉬프트 + f10을 해주면 최근 실행한 메소드를 실행해주는것같다.

 

여러개의 테스트 메소드를 한번에 돌리고싶다면 클래스 옆에있는 run을 누르던지 클래스에서 컨트롤 + 쉬프트 + f10 하면 여러개의 메소드를 동시에 테스트 할 수 있다.

 

findAll까지 구현하고 전체 테스트를 해보면 findbyName에서 오류가발생한다.

오류의 이유는 findAll을 진행할때(테스트 자바파일에서 findAll테스트를 위해 멤버 2개를 집어넣는 코드를 진행했었음)

저장소(테스트 자바파일의)에 이미 member1과 2를 저장해놨는데

findbyname에서 멤버 1,2를 또 저장하고 꺼내오려니 이미 저장된 애들을 가져오게되고(findAll할때 저장해놨던),

예상치못한 객체가 나와서 오류가 발생하는것.(값이 같아도 객체는 주소값이 다르므로,주소값이 다르다 == 다른객체)

전체 테스트할때 메소드 동작순서는 랜덤이다. 그러므로 메소드를 만들때 동작순서에 상관없이 만들어야함.

해결방법은 테스트가 한번끝나고 나면 데이터를 클리어해줘야한다. (저장소에 저장된 데이터를 지워되야한다.)

public void clearStore() { //인터페이스에 있는걸 구현한게아닌 해당 클래스에서 구현한 고유의 메소드
    store.clear(); //
}

다음과 같은 메소드를 MemoryMemberRepository.java에 추가해준다. 

Map 변수를 저장소로 사용하고 있는데 Map에 저장된 모든 객체를 삭제해주는 메소드이다.

 

다음과 같은 메소드를  @AfterEach를 붙인 메소드에 넣어주면 , 하나의 테스트 메소드가 동작끝날떄마다 해당 메소드를 실행하게 된다.

@AfterEach //하나의 테스트 메소드가  동작이 끝날때마다 해당 어노테이션이 붙은 메소드를 동작하라는뜻.
public void afterEach()
{
    repository.clearStore(); // 저장소에있는 내용 삭제
}

@AfterEach 어노테이션과 다른 어노테이션에 관한 정보참조

 

 

[JUnit5] JUnit5 기본 어노테이션 (@Test / @BeforeAll / @AfterAll / @AfterEach / @BeforeEach / @Disabled)

안녕하세요. 오늘은 JUnit 5버전에 대해서 포스팅하고자 합니다. 현재 스프링부트 버전이 2.3버전이 나온걸로 알고 있습니다. 기본적으로 스프링 부트 프로젝트를 생성하니 2.3이라고 적혀있더라

sas-study.tistory.com

 

이렇게 코드를 다 짜고 난후  테스트에 대한 코드를 짜서 테스트를 이용해 실험하는 방법도 있지만

테스트 코드를 먼저짜고 개발을 시작하는 TDD 테스트 주도개발이라는것도 존재한다. 

 

나중에 테스트 파일이 너무많을대는 패키지폴더에서 Run을 해버리면된다.

Test경우에는 패키지에서 Run을 해버릴수있다.

댓글