[스프링 입문] 4일차 - 비즈니스 요구사항 정리, 회원 도메인과 레포지토리 만들기, 테스트 케이스 작성

tdddt·2024년 2월 9일
0

스프링 입문

목록 보기
4/13

비즈니스 요구사항 정리

  • 데이터 : 회원ID, 이름
  • 기능 : 회원 등록, 조회
  • DB : 아직 DB가 선정되지 않음(가상의 시나리오) -> 성능이 중요한 DB, 관계형 DB, No SQL로 할 지 아직 안 정해진 상황

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

  • 컨트롤러 : 웹 MVC or API의 컨트롤러 역할,
  • 서비스 : 비즈니스 도메인 객체를 가지고, 핵심 비즈니스 로직이 동작하도록 구현
    ex) 회원은 중복 가입이 안 됨
  • 리포지토리 : DB에 접근, 도메인 객체를 DB에 저장하고 관리
  • 도메인 : DB에 주로 저장하고 관리되는 비즈니스 도메인 객체
    ex) 회원, 주문, 쿠폰
  • DB

클래스 의존관계

  • MemberService : 회원 비즈니스 로직
  • MemberRepository : 회원 저장, interface로 설계(아직 데이터 저장소가 선정되지 않았기 때문)
    | 아직 데이터 저장소 선정 X -> 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계
  • MemoryMemberRepository : 구현체,
    | 개발을 진행을 위해 초기 개발 단계에서는 메모리에 넣었다 뺐다 할 수 있는 가벼운 메모리 기반 구현체 설계

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

  • domain 패키지 생성 > hello.hellospring.domain
  • domain 패키지 아래 Member 클래스 생성
public class Member {
    private Long id; // id 식별자(시스템이 저장하는 id)
    private String name; // 이름
	
    // cmd+N : Getter and Setter 생성
    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 패키지 생성 > hello.hellospring.repository
  • repository 패키지 아래 MemberRepository 인터페이스 생성
    • Optional : return값이 null인 경우 그대로 반환하는 게 아니라 Optional로 감싸서 반환
public interface MemberRepository {
	Member save(Member member); // 멤버 저장
    Optional<Member> findById(Long id); // id로 멤버찾기
    Optional<Member> findByName(String name); // 이름으로 멤버찾기
    List<Member> findAll(); // 지금까지 저장된 모든 회원 리스트 반환
}
  • repository 패키지 아래, 구현체 MemoryMemberRepository클래스 생성
// implements MemberRepository > opt+enter > Implement methods > 전부 선택 후 ok
public class MemoryMemberRepository implements MemberRepository{
	// 저장소 만들기 : 이후 ctrl+space나 option+enter로 import
    // 실무에서는 동시성 문제가 있을 수 있어서 이렇게 공유되는 변수일 때는 concurrent HashMap을 써야하지만, 예제니까 단순하게 HashMap 사용
    private static Map<Long, Member> store = new HashMap<>();
    
    // Sequence(0,1,2와 같이 키 값을 생성해줌) 만들기 : 
    // 실무에서는 Atomic Long 등등을 해야하지만 예제이므로 단순하게 구현
    private static long sequence = 0L;

    @Override 
    public Member save(Member member) {
    // sequence값 증가 후 member의 id로 자동 저장, store 저장소에 저장.
        member.setId(++sequence); // 증가 후 저장
        store.put(member.getId(),member);
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
    // null값이 나올 가능성이 있는 변수를 Optional.ofNullable로 감싸서 반환
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public Optional<Member> findByName(String name) {
        return store.values().stream()
                .filter(member -> member.getName().equals(name)) // member의 name이 파라미터로 넘어온 name과 같은지 확인
                .findAny(); // 하나라도 찾으면 그 결과를 그대로 반환 (return값 Optional 설정)
    }

    @Override
    public List<Member> findAll() {
    // store은 Map인데 반환은 List로 설정(자바 실무에서 리스트를 많이 쓰기 때문)
        return new ArrayList<>(store.values());
    }
    
    public void clearStore() {
         store.clear();
	}
}

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

보통의 테스트 방법 : main 메소드를 통해서 실행/웹 애플리케이션의 컨트롤러를 통해서 실행
-> 준비하고 실행하는데 오래 걸리고, 반복해서 실행하기 어렵고, 여러 테스트를 한 번에 실행하기 어렵다는 단점 !

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


회원 레포지토리 메모리 구현체 테스트

  • src/test/java/hello.hellospring아래에 repository 패키지 생성 (보통 똑같으 패키지 이름으로 생성)
  • 패키지 아래에 MemoryMemberRepositoryTest 클래스 생성
package hello.hellospring.repository;

import hello.hellospring.domain.Member;
//import org.junit.jupiter.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 {
    MemoryMemberRepository repository = new MemoryMemberRepository();

    @AfterEach // 메소드가 실행이 끝날 때마다 동작
    public void afterEach(){
        repository.clearStore();
    }

    @Test // @Test를 붙이면 바로 메소드 실행 가능
    public void save(){
        Member member = new Member();
        member.setName("spring");

        // member 저장
        repository.save(member);

        // findById의 반환타입 : Optional
        // Optional에서 값을 꺼낼 때는 get 메소드 활용
        Member result = repository.findById(member.getId()).get();

        // System.out.println("result = "+(result ==member));
        // 이렇게 직접 출력하는 방식보다 Assert를 주로 사용함
        
        // org.junit.jupiter.api
        // Assertions.assertEquals(member, result); // (expected, actual)
       
       	// org.assertj.core.api : 요즘에 더 자주 사용
        // Assert에 커서를 두고 opt+ontion > Add on-demand static import를 클릭하면 다음부터는 아래와 같이 assertThat으로 바로 사용 가능
        // 실무에서는 Build 툴이랑 엮어서 오류 테스트 케이스를 통과하지 않으면 다음 단계로 못 넘어가게 막아버림.
        assertThat(member).isEqualTo(result);
    }

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

		// Shift+F6 : rename 단축키
        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();
        member2.setName("spring2");
        repository.save(member2);

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

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

테스트 케이스 장점

클래스 레벨에서 돌리거나 좌측 폴더에서 테스트를 하면 전체 클래스를 다 테스트 해 볼 수 있음.


테스트 케이스 작성 시 고려사항

💡테스트는 각각 독립적으로 실행되어야 한다.
💡테스트 순서에 의존관계가 있는 것은 좋은 테스트가 아니다.

  • 테스트 케이스의 실행 순서는 보장할 수 없다. 즉, 모든 테스트는 순서와 상관없이, 서로 의존관계가 없도록 설계되어야 한다. (메소드 별로 전부 따로 동작하도록)
  • 이를 고려하지 않고 한 번에 여러 테스트를 실행하면 메모리 DB에 직전 테스트의 결과가 남아 다음 테스트가 실패할 가능성이 있다.
  • 이를 막기 위해서는 하나의 테스트가 끝낼 때마다 저장소나 공용 데이터들을 깔끔하게 지워줘야 한다.
  • 이때 사용할 수 있는 게, @AfterEach. 각 테스트가 종료될 때마다 기능이 실행된다.

구현 클래스와 테스트 클래스 순서

실습 방법은 구현클래스를 먼저 만들고 테스트 클래스를 작성하는 방식으로 이루어졌으나, 테스트 클래스를 먼저 작성 후, 구현 클래스를 작성하는 방법도 있다. 이 방법을 테스트 주도 개발, TDD라고 한다.

0개의 댓글