목차
0. 환경
- m1 macbook
- IntelliJ IDEA(m1) - 202102
- java 11(AdoptOpenJDK-11.0.11)
- 자바를 설치하지 않았다면 아래의 링크를 활용해주세요.
1. 비즈니스 요구사항 정리
[비즈니스 요구사항]
- 데이터: 회원 ID, 이름
- 기능: 회원 등록, 조회
- 저장소: 아직 데이터 저장소가 선정되지 않음(가상의 시나리오)
[상세 설명]
- 아직 데이터 저장소가 선정되지 않아서, 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계합니다.
- 데이터 저장소는 RDB, NoSQL 등등 다양한 저장소를 고민 중인 상황으로 가정합니다.
- 개발을 진행하기 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용합니다.
- private static Map<Long, Member> store = new HashMap<>();
- private static long sequence = 0L;
- 실습이라 Map과 long을 사용했는데 공유되는 변수 같은 경우엔 동시성 문제로 실무에서는 ConcurrentHashMap, AtomicLong 사용을 권장합니다.
[웹 애플리케이션 계층 구조]
- 컨트롤러: MVC의 컨트롤러 역할
- 서비스: 핵심 비즈니스 로직 구현
- 리포지토리: DB에 접근, 도메인 객체를 db에 저장하고 관리
- 도메인: 비즈니스 도메인 객체 (회원 주문 쿠폰처럼 DB에 주로 저장되고 관리됨)
[클래스 의존 관계]
- { class MemberService } = 회원 서비스
- { interface MemberRepository } = 데이터 저장소가 선정되지 않아서 우선 인터페이스 설계
- { class MemoryMemberRepository implements MemberRepository } = 구현체
2. 회원 도메인과 리포지토리 만들기
- 회원 객체, 회원 리포지토리 인터페이스, 회원 리포지토리 메모리 구현체를 작성합니다.
2.1. 회원 객체
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;
}
}
- src/main/java/hello.hellospring.domain에 Member 클래스를 생성 후 작성합니다.
- 단축키(인텔리제이 + mac 기준)
- [control] + [enter]로 getter, setter 편하게 생성 가능합니다.
2.2. 회원 리포지토리 인터페이스
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();
}
- src/main/java/hello.hellospring.repository에 MemberRepository 인터페이스를 생성 후 작성합니다.
- save(), findById(), findByName(), findAll()
- Optional<T>는 null이 올 수 있는 값을 감싸는 Wrapper 클래스로, 참조하더라도 NPE가 발생하지 않도록 도와준다.
- NPE(NullPointerException)
2.3. 회원 리포지토리 메모리 구현체
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.*;
/**
* 동시성 문제가 고려되어 있지 않음, 실무에서는 ConcurrentHashMap, AtomicLong 사용 고려
*/
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) {
return Optional.ofNullable(store.get(id)); //23line
}
@Override
public List<Member> findAll() {
//자바에서 반환할 때 리스트를 많이 씀(루프 돌리기 편한 이유 등)
return new ArrayList<>(store.values());
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name)) //35line
.findAny(); //36line
}
public void clearStore() {
store.clear();
}
}
- src/main/java/hello.hellospring.repository에 MemoryMemberRepository 클래스를 생성 후 작성합니다.
- 단축키(인텔리제이 + mac 기준)
- [option] + [enter] 오버라이드 메소드 한번에 가지고 올 수 있음(implement method)
- [option] + [enter]를 이용하면 import 편하게 할 수 있습니다.
- 가벼운 메모리 기반의 데이터 저장소 사용
- private static Map<Long, Member> store = new HashMap<>();
- private static long sequence = 0L;
- 공유되는 변수 같은 경우엔 동시성 문제를 조심해야 합니다.
- HashMap 보다는 ConcurrentHashMap
- long 보다는 AtomicLong
- Optional.ofNullable 사용해 NULL을 대비할 수 있습니다. (소스코드 23번 라인)
- Incompatible types. Found: 'hello.hellospring.domain.Member', required: 'java.util.Optional<hello.hellospring.domain.Member>'
- findByName() 람다 표현식
- 소스코드 35번 라인: .filter(member -> member.getName().equals(name)) // 같은지 필터링하고 찾으면 반환
- 소스코드 36번 라인: .findAny(); // 끝까지 확인해봤는데도 없으면 Optional에 null을 넣어서라도 반환을 하게 됨
[ConcurrentHashMap]
- HashMap을 thread-safe 하도록 만든 클래스
- HashMap과는 다르게 key, value에 null을 허용하지 않습니다.
- 실무에서는 동시성 문제로 공유되는 변수일 경우 사용하게 됩니다.
[AtomicLong]
- AtomicLong은 Long 자료형을 갖고 있는 Wrapping 클래스
- Thread-safe로 구현되어 멀티스레드에서 synchronized 없이 사용할 수 있습니다.
- synchronized 보다 적은 비용으로 동시성을 보장할 수 있습니다.
3. 회원 리포지토리 테스트 케이스 작성
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();
@AfterEach //16line
public void afterEach() {
repository.clearStore();
}
@Test
public void save() {
//given
Member member = new Member();
member.setName("spring");
//when
repository.save(member);
//then
Member result = repository.findById(member.getId()).get();
assertThat(result).isEqualTo(member);
}
@Test
public void findByName() {
//given
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member(); //42line
member2.setName("spring2");
repository.save(member2);
//when
Member result = repository.findByName("spring1").get();
//then
assertThat(result).isEqualTo(member1);
}
@Test
public void findAll() {
//given
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
//when
List<Member> result = repository.findAll();
//then
assertThat(result.size()).isEqualTo(2);
}
}
- src/test/java.hello.hellospring.repository에 MemoryMemberRepositoryTest 클래스를 생성 후 작성합니다.
- @Test(org.junit.jupiter.api) 어노테이션을 이용해 실행이 가능합니다.
- 검증 시 출력을 통해 확인할 수도 있지만 항상 출력으로 다 확인을 할 수 있는 것은 아니기에 Assertion 클래스를 이용하면 편하게 결과 데이터를 검증할 수 있습니다.
- 출력 방식 검증
- System.out.println("result = " + (result == member));
- import Assertions(org.junit.jupiter.api)
- Assertion.assertEquals(result, member); // 이런 식으로 데이터 검증도 가능 하지만 가독성 때문에 소스코드 32번 라인처럼 사용하는 게 좋습니다.
- import Assertion(org.assertj.core.api)
- assertThat(result).isEqualTo(member); //소스코드 32번 라인
- 출력 방식 검증
- 단축키(인텔리제이 + mac 기준)
- 소스코드 42번 라인
- Member member2 = new Member(); 멤버를 추가하는 경우 위의 코드를 복사해서 변수명을 Rename을 해줘야 하는데 해당 변수에 커서를 올리고 [shift] + [f6]을 하면 편하게 rename을 할 수 있습니다.
- @AfterEach
- 소스코드 16번 라인
- 한 번에 여러 테스트를 실행하면 메모리 DB에 직전 테스트의 결과가 남을 수 있습니다. 그렇게 되면 다음 테스트에서 에러가 발생할 수 있습니다.
- 테스트가 끝날 때마다 데이터를 지워줘야 하므로 @AfterEach 어노테이션을 이용해 각각의 테스트(메소드)가 끝날 때마다 실행해줍니다.
- 전체 테스트 시 각 테스트(메소드)는 순서 보장이 안됩니다.(순서 의존 X)
- 테스트는 각각 독립적으로 실행되어야 합니다. 테스트 순서에 의존관계가 있는 것은 좋은 테스트가 아닙니다.
[테스트 케이스 코드의 필요성]
테스트 케이스를 이용해 테스트를 하지 않을 경우 보통 자바의 main 메서드를 통해서 실행하거나, 웹 애플리케이션의 컨트롤러를 통해서 해당 기능을 실행합니다. 이러한 방법은 준비하고 실행하는데 오래 걸리고, 반복 실행하기 어렵고 여러 테스트를 한 번에 실행하기 어렵다는 단점이 있습니다. 자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결할 수 있습니다. 혼자서 개발하는 경우엔 문제가 없을 수 있지만 다수가 개발하고 소스코드가 많고 길어지는 경우 테스트 케이스 코드가 더욱더 필요하게 됩니다.
프로젝트 중 테스트 케이스 먼저 작성 후 하기도 합니다. 이러한 것을 테스트 주도 개발 (TDD)라고 합니다.
'Backend > 코드로 배우는 스프링 부트' 카테고리의 다른 글
[코드로 배우는 스프링 부트] 5. 스프링 빈과 의존관계 (자바 코드로 직접 스프링 빈 등록하기) (0) | 2021.11.01 |
---|---|
[코드로 배우는 스프링 부트] 4. 스프링 빈과 의존관계 (컴포넌트 스캔과 자동 의존관계 설정) (0) | 2021.10.26 |
[코드로 배우는 스프링 부트] 3-2. 회원 관리 예제 (서비스, DI) (0) | 2021.10.25 |
[코드로 배우는 스프링 부트] 2. Spring 웹 개발 기초(정적 컨텐츠, MVC, API) (0) | 2021.10.17 |
[코드로 배우는 스프링 부트] 1. SpringBoot 프로젝트 생성 (0) | 2021.10.11 |