일반적인 웹 어플리케이션 계층 구조
- 컨트롤러 : 웹 MVC의 컨트롤러 역할
- 서비스 : 핵심 비즈니스 로직 구현
- 리포지토리 : 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리
- 도메인 : 비즈니스 도메인 객체
클래스 의존 관계
- 아직 데이터 저장소가 선정되지 않아서, 우선 인터페이스로 구현 클래스를 변견할 수 있도록 설계
- 데이터 저장소는 RDB, NoSQL 등 다양한 저장소를 고민 중인 상황으로 가정
- 개발을 진행하기 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용
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();
}
- Optional : Java8 이상 있는 기능. null로 반환하는 경우 주로 Optional로 감싸서 반환함
- Optional의 ofNullable()을 사용하면 null이 나와도 에러가 나지 않고 처리 가능
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.*;
public class MemoryMemberRepository implements MemberRepository{
//DB 역할
private static Map<Long, Member> store = new HashMap<>();
//PK 역할
private static long sequence = 0L;
...
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
...
}
- ofNullable(): null이 나와도 에러가 나지 않고 처리 가능
회원 리포지토리 테스트 케이스 작성
개발한 기능을 실행해서 테스트 할 때 자바의 main 메소드를 통해 실행하거나
웹 어플리케이션의 컨트롤러를 통해서 해당 기능을 실행한다
이런 방법은 준비하고 실행하는 데 오래 걸리고,
반복 실행하기 어려우며 여러 테스트를 한번에 실행하기 어렵다는 단점이 있다
자바는 JUnit 이라는 프레임워크로 테스트를 실행해
이러한 문제를 해결한다
스프링은 테스트코드를 작성할 수 있도록 환경 세팅을 프로젝트 생성 시 미리 해준다
test 패키지에 작성
https://github.com/NayoungBae/springIntroduction/commit/10883b623cd424854a9924efbae467c42df72e1c
findByName() 함수에서 오류 발생. 왜?
- 테스트함수는 실행 순서가 보장되지 않음
- findByName 이전에 실행된 코드에서 저장소에 데이터를 저장했기 때문에, 저장소가 비어있지 않은 상태에서 findByName을 실행했기 때문
- 해결책 : 하나의 테스트 함수를 실행 완료한 뒤 저장소 데이터를 지우는 코드 작성
- https://github.com/NayoungBae/springIntroduction/commit/64d6a837dceeba70876d3eeb11436740200769ec#diff-c4e084f0df42e8f8b1e08764c4d432fb67983baa44219120ba676ad0a9c9dc81
- 리포지토리에서 HashMap을 비우는 함수 clear를 실행시키는 메소드를 만들고
-
public void clearStore() { store.clear(); }
- 테스트코드 작성 시 @AfterEach 어노테이션을 이용해 clearStore 함수를 매번 실행
-
@AfterEach //하나의 테스트가 끝난 뒤 실행 public void afterEach() { repository.clearStore(); }
지금까지 리포지토리를 작성하고, 만든 리포지토리를 테스트하기 위해 테스트코드를 작성했다
하지만 반대로 하는 방법도 있다
테스트코드를 작성한 다음, 그 틀에 맞추어 실제로 쓰이는 코드를 작성하는 방법
이것을 TDD(테스트 주도 개발)라고 함
https://nazero.tistory.com/136
회원 서비스 개발
https://github.com/NayoungBae/springIntroduction/commit/ac37897b81613886b36dd3de3de6cdf78cf39194
회원가입 로직
- 같은 이름이 있는 중복 회원은 회원가입 불가
-
public Long join(Member member) { validateDuplicateMember(member); //중복 회원 검증 memberRepository.save(member); return member.getId(); } private void validateDuplicateMember(Member member) { //같은 이름이 있는 중복 회원은 안된다 memberRepository.findByName(member.getName()) .ifPresent(member1 -> { throw new IllegalStateException("이미 존재하는 회원입니다."); }); }
- ifPresent 사용법 : https://www.whiteship.me/optional-ifpresent/
요즘에는
if(member != null) {
} else {
}
이런 식으로 짜는 게 아닌
Optional<Member> result = ...;
result.ifPresent(member -> {
});
이런 식으로 Optional에서 제공해주는 함수를 사용한다
get 함수를 이용해서 결과 데이터를 직접 꺼내는 것은 권장하지 않는다
orElseGet()도 자주 사용한다고 한다
- Optional로 바로 반환하는 건 좋지 않음
-
//개선 전 코드 private void validateDuplicateMember(Member member) { //같은 이름이 있는 중복 회원은 안된다 Optional<Member> result = memberRepository.findByName(member.getName()) result.ifPresent(member1 -> { throw new IllegalStateException("이미 존재하는 회원입니다."); }); }
-
//개선된 코드 private void validateDuplicateMember(Member member) { //같은 이름이 있는 중복 회원은 안된다 memberRepository.findByName(member.getName()) .ifPresent(member1 -> { throw new IllegalStateException("이미 존재하는 회원입니다."); }); }
회원 서비스 테스트
https://github.com/NayoungBae/springIntroduction/commit/a6b098570a9de26c53fd7bd15ac0d85c141f40d3
- 테스트 코드 작성 시 given, when, then 순서대로 적으면 좋음
정상 케이스 작성도 중요, 예외 케이스도 중요!
https://github.com/NayoungBae/springIntroduction/commit/fbbc2adc5d532b7dff0f1411530f60e854c61bac
- 예외처리 테스트 시 제공하는 문법이 있으니, try-catch를 쓸 필요 없음
-
//개선 전 //when memberService.join(member1); try { memberService.join(member2); fail("예외가 발생해야 합니다."); } catch(IllegalStateException e) { assertEquals(e.getMessage(), "이미 존재하는 회원입니다."); }
-
//개선 후 //when memberService.join(member1); //이 로직 실핼 시 이런 에러를 기대한다 IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2)); //then //메시지 검증 assertEquals(e.getMessage(), "이미 존재하는 회원입니다.");
https://github.com/NayoungBae/springIntroduction/commit/6befb96e65f280b79772bf8fd4f05b7029ac78be
현재 MemberService와 MemberServiceTest에서 쓰이는 리포지토리가 각자 다름!
따지고보면 각자 다른 저장소를 쓰고 있는 것임!
한 프로젝트에서 똑같은 저장소를 써야 하는데 다른 저장소를 새로 만들어서 사용하는 꼴이 되어버림
class MemberServiceTest {
//여기 안에 또다른 MemoryMemberRepository가 있음
MemberService memberService = new MemberService();
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
...
}
public class MemberService {
private final MemberRepository memberRepository =
new MemoryMemberRepository();
...
}
해결 방법 : 의존성 주입! DI!
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
...
}
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
...
}
'Spring Framework' 카테고리의 다른 글
Spring) 회원 관리 예제 - 홈 화면 추가 / 회원 웹 기능 - 등록 (0) | 2022.02.01 |
---|---|
Spring) 스프링 빈과 의존관계 (0) | 2022.01.22 |
Spring) API (0) | 2022.01.21 |
Spring) 빌드하고 실행하기 / 정적 컨텐츠 / MVC와 템플릿 엔진 (0) | 2022.01.08 |
Spring) 스프링부트 프로젝트 생성 / 라이브러리 살펴보기 / Veiw 환경설정 (0) | 2022.01.03 |