인프런에서 무료로 열려 있는 스프링부트 입문 강의를 수강하고 있다. 오늘 배운 내용은 회원 관리에 필요한 백엔드를 만드는 것이다.
먼저 다음과 같은 상황을 가정했다.
- 데이터에는 회원 ID(pk로 사용자가 아닌 시스템에서 고유키로 부여), 회원 이름을 저장
- 기능 : 회원 등록, 조회
- 아직 데이터베이스로 무엇을 사용해야 할지 (ex. MSSQL, MySQL 등) 결정하지 못해서 DB가 없이 먼저 개발부터 해야 할 상황
일반적으로 웹 애플리케이션의 계층 구조는 다음과 같다.
이 때 아직 DB가 선정되지 않았기 때문에, 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계하며, 구현체로 가벼운 메모리 기반의 데이터 저장소를 사용한다.
※자바의 인터페이스※
객체와 객체 사이에서 일어나는 상호 작용의 매개로 쓰인다. 여기서는 사용되는 컨트롤러 내의 메소드들을 미리 선언해둔 형태로 쓰였는데, 인터페이스를 사용하는 이유는 이 블로그를 보니 좀 더 확실하게 이해가 되었다. 쉽게 말해서, 미리 선언해두어서 협업도 편리하게 하고 (협업할 때는 메소드 이름부터 반환값까지 동일하게 해줘야 하는데 이 과정을 인터페이스로 미리 정해둘 수가 있으니까) 교체도 용이하게 한다.
이제 회원 도메인과 리포지토리를 만들어주면 된다. 먼저 도메인에서 회원 클래스에 대해 정의해준다. 도메인 패키지를 생성해주고, member 클래스를 추가해서 아래 코드와 같이 정의해준다.
package hello.hellospring.domain;
public class Member {
private Long id;
private String name;
public Long getId() {
return id;
}
public Member setId(Long id) {
this.id = id;
return null;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
그 다음은 레포지토리 패키지를 생성해서 인터페이스를 선언해주고 메모리 구현체(메소드 정의)를 작성해준다.
// repository 패키지 내의 인터페이스
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); //Id나 이름으로 찾을 때 null 값이 반환될 수 있는데, 이 때 그냥 null을 반환하지 않고 optional로 감싸서 반환해줌.
Optional<Member> findByName(String name);
List<Member> findAll();
}
// 메모리 구현체 클래스
// 현업에서는 동시 접근 문제를 위해 ConcurrentHashMap, AtomicLong를 사용하지만 예시에서는 단순하게 접근
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.*;
public class MemoryMemberRepository implements MemberRepository{
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);
return member;
}
@Override
public Optional<Member> findById(Long id) {
//원래는 store.get(id)로만 불러와도 되지만 null 값이 반환될 가능성이 있을 경우 optional로 감싸줌.
return Optional.ofNullable(store.get(id));
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny(); //루프를 돌면서 하나라도 찾으면 return, 없으면 Optional을 감싼 Null로 반환.
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
}
이렇게 작성한 코드가 존재하면 정상적으로 작동하는지 확인을 해야 한다. 이 때 사용하는게 바로 테스트 케이스이다. 물론 main 메소드를 이용해서 테스트를 할 수도 있지만 이렇게 진행하게 되면 준비하고 실행하는 데 오래 걸리고, 반복 실행하거나 여러 테스트를 한 번에 실행하기가 어렵다. 자바는 JUnit 이라는 프레임워크로 테스트를 진행해서 이러한 문제를 해결한다.
먼저 src/test/java 하위폴더에 이름을 소스코드와 동일하게 맞춘 repository 패키지를 생성했다. 그러고 난 후에 소스 코드 자바 클래스 파일과 동일한 이름의 Test 클래스 파일을 생성해준다.
테스트 케이스는 아래와 같이 작성해준다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
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 {
MemoryMemberRepository repository = new MemoryMemberRepository();
//test가 끝나면 레포지토리(공용으로 사용되는 요소들 등)를 비워줘서 다음 테스트를 원활하게 수행할 수 있도록 설정해줘야 함.
@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();
//optional에서 값을 꺼낼 때는 get()을 이용해서 바로 꺼낼 수 있음.
// assertions는 assertj에서 import 해오는 것을 많이 사용
assertThat(member).isEqualTo(result); // opt + enter 쳐서 static import 하면 Assertions. 부분을 생략 가능
//실무에서는 build 툴과 엮어서 테스트 케이스를 통과하지 않으면 다음단계로 못 넘어가게 막아버리기도 함.
}
@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("Spring2").get();
assertThat(result).isEqualTo(member2);
}
@Test
public void findAll(){
Member member1 = new Member();
member1.setName("Spring");
repository.save(member1);
Member member2 = new Member();
member2.setName("Spring1");
repository.save(member2);
Member member3 = new Member();
member3.setName("Spring2");
repository.save(member3);
List<Member> result = repository.findAll();
assertThat(result.size()).isEqualTo(3);
} // test 순서는 보장되지 않음. 따라서 테스트 케이스 설계시 순서와 관련없이 테스트가 가능하도록 설계해야 함
}
* Assertions 는 서로 동일한지 확인해 줄 수 있다. System.out.println 을 이용하여 확인해도 되지만 Assertions를 통해서 테스트 케이스를 돌려보는 경우가 많다.
** null 값을 Optional로 감쌌는데, 이는 결과 값을 가져올 때, .get()을 이용하면 Optional 객체가 아닌 그 안의 값을 가져올 수 있다.
*** 현재 테스트 케이스를 세 가지를 생성해서 돌렸는데 전체 테스트를 돌리게 되면 코드 라인 순서대로 실행되는 것이 아니다. 따라서 케이스 별로 순서에 종속적이지 않도록 해줘야 하는데, 이 때 공용으로 사용되는 배열이나 리스트 등을 비워줘야 한다. 따라서 여기서는 afterEach() 메소드를 이용해서 비워줬다.
케이스마다 테스트 하는 방법
실행할 케이스 옆의 초록 버튼을 누르고 run 해줄 수 있다.
전체 테스트 케이스 전부 돌리는 방법
이렇게 테스트 케이스를 작성해줄 수 있는데, 만약 개발보다 테스트 케이스를 먼저 작성해주면 그게 TDD(Test Driven Development, 테스트 주도 개발) 방식인 것이다. 이렇게 하려면 내가 어떤 것을 어떻게 개발할지에 대한 명확한 그림을 가지고 있어야 할 것 같다.
다음으로는 계속해서 회원 서비스 개발과 회원 서비스 테스트를 진행해보려고 한다.
'TIL > Java | Spring Boot' 카테고리의 다른 글
[Spring Boot] 스프링 빈과 의존관계 (0) | 2022.01.09 |
---|---|
[Spring Boot] 간단한 예제 - 회원 관리 예제 2 (0) | 2022.01.07 |
[Spring Boot] 서버에서 스프링 파일 실행시키기 (0) | 2022.01.03 |
[Spring Boot] spring-boot-devtools 라이브러리 추가 (0) | 2022.01.03 |
2022.1.1 TIL : [Java] 컬렉션과 제네릭 (Collection & Generic) - 마무리 (0) | 2022.01.01 |
댓글