스프링 입문 - 회원 관리 예제
이 글은 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 커리큘럼에 맞춰서 작성된 글입니다😀
비즈니스 요구사항 정리
스프링을 이용해 전체적으로 어떤 방식으로 개발이 이루어지는지 알아보기 위해 간단한 요구사항으로 설정한다.
때문에 데이터는 회원ID와 이름을, 기능은 회원 등록과 조회만 지원한다.
그리고 스프링의 특성들을 잘 설명하기 위해 데이터 저장소가 선정되지 않은 상황이라고 가정하자.
일반적인 웹 애플리케이션 구조로는 컨트롤러, 서비스, 리포지토리, 도메인, DB로 구성돼 있다.
- 컨트롤러 : 웹 MVC의 컨트롤러 역할
- 서비스 : 핵심 비즈니스 로직 구현
- 리포지토리 : 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리
- 도메인 : 비즈니스 도메인 객체 예) 회원, 주문, 쿠폰 등 주로 데이터베이스에 저장하고 관리됨
클래스 의존관계에서는
- 아직 데이터 저장소(DB)가 선정되지 않아서, 우선 인터페이스를 구현 클래스로 변경할 수 있도록 설계
- 데이터 저장소는 RDB, NoSQL 등 다양한 저장소를 고민중인 상황으로 가정
- 개발 진행을 위해 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용
회원 도메인과 레포지토리 만들기
회원 도메인은 src/main/java/hello/hellospring/domain
에서 관리하고,
회원 레포지토리는 src/main/java/hello/hellospring/repository
에서 관리하도록 한다.
먼저 회원 관련 정보를 만들기 위해 domain 패키지에 클래스 Member.java
를 만든다.
위에서 회원ID와 이름을 다룬다고 했으니 필드에 id와 name을 만들고 getter와 setter도 만들어주자.
그리고 repository 패키지엔 회원 등록과 조회기능을 지원하는 인터페이스를 먼저 만든다.
findAll()
은 전체 회원의 정보를 가져오는 메소드이다.
바로 위에서 만든 인터페이스를 구현하는 MemoryMemberRepository.java
를 같은 경로에 만들어주자.
Map 컨테이너인 store에 멤버들을 저장하고 관리하며, sequence
는 각각 지정될 id를 의미한다고 보면 된다.
인자로 들어오는 값이 null이 되는 경우 직접 처리해줄 수도 있겠지만 그런 번거로움을 줄이기 위해서
Optional을 사용하는 것으로 보인다. 자세한 내용은 여기를 참고!
회원 리포지토리 테스트 케이스 작성
작성된 코드를 검증하는 단계로, main 메소드나 웹 애플리케이션의 컨트롤러를 통해 실행하는 방법이 있는데
이 방법은 준비 및 실행 시간이 오래걸리고 반복 실행하기 어려워 JUnit이라는 프레임워크를 이용한다.
test/java/hello/hellospring
에 이름이 repository인 레포지토리를 생성해주자.
그리고 해당 경로에서 테스트할 대상은 MemoryMemberRepository이므로 클래스를 다음과 같이 생성해주자.
test/java/hello/hellospring/repository/MemoryMemberRepositoryTest.java
MemoryMemberRepository.java
에서 구현했던 save(), findByName(), findAll()에 대해 테스트할 것이다.
따라서 MemoryMemberRepositoryTest.java
에서도 똑같이 해당 메소드들을 적어주는데,
@Test라는 어노테이션을 꼭 붙여줘야 한다!
@Test
public void save() {
...
}
@Test
public void findByName() {
...
}
@Test
public void findAll() {
...
}
모든 메소드 바디에는 테스트하기 위한 내용이 들어갈텐데 전체적으로 비슷비슷하다!
일단 내용을 채우기 앞서서 Member
내용을 담아줄 MemoryMemberRepository
타입의 repository
를 만들어주자.
MemoryMemberRepository repository = new MemoryMemberRepository();
이후 save()에서는 새 Member
를 만들고 이름을 “spring”으로 지정한 후 repository에 저장한다.
그리고 집어넣은 객체 member
의 id를 갖고 repository
에 있는 Member
중 id가 같은 객체를 꺼내
result
라는 객체에 저장한다. 최종적으로 result
와 member
가 같은지 비교함으로써 테스트가 끝난다.
비교는 Assertions.assertThat(객체1).isEqualTo(객체2)
를 통해 이루어지며 결과는 Run을 해보면 알 수 있다.
findByName()와 findAll() 구현은 다음과 같다.
근데 test 폴더에 우클릭을 하고 Run ‘All Tests’를 누르면 에러가 뜨게 된다!
정확히 말하면 findAll()를 작성하기 전 즉, save()와 findByName()만 작성했을 때는 문제가 없었으나
findAll()를 작성하고 Run 하면 문제가 생기게 된다.
왜 이런 문제가 발생할까? 다음 오류 화면에서 메소드의 실행 순서를 확인하자!
작성한 순으로 메소드가 실행되지 않기 때문에 문제가 발생한 상황이다.
더 정확하게 말하면 findAll()에서 “spring1”이라는 이름을 가진 Member
를 repository
에 넣었고
그 뒤에 실행되는 findByName()에서 같은 이름의 Member
를 repository
에 넣었기 때문에
result가 findAll()에서 집어넣은 Member
가 되면서 assertThat()
에서 에러를 뱉게 되는 것이다.
결론은 메소드 실행 순서는 작성순으로 보장이 되지 않으므로 매번 초기화 해주는 작업이 필요하다.
MemoryMemberRepositoryTest.java
에는 afterEach()
를,
MemoryMemberRepository.java
에는 clearStore()
를 구현해주자.
이제 실행하면 매번 초기화 작업을 하므로 실행 순서에 영향을 받지 않게 된다.
회원 서비스 개발
서비스 로직을 구현하는 단계다.
src/main/java/hello/hellospring
에 service라는 리포지토리를 만들고, MemberService
라는 클래스를 만들자!
위에서 봤던 MemoryMemberRepositoryTest.java
처럼 memberRepository
를 필드에 선언해서 이용할 것이다.
private final MemberRepository memberRepository = new MemoryMemberRepository();
회원 가입 서비스를 구현하기 위해 join()이라는 메소드를 만들어보자.
코드에는 valilast_modified_atDuplicateMember()가 들어가 있는데 이름이 중복이 되지 않도록 체크하는 로직이다!
주석으로 처리된 부분을 부연 설명하면, 결과값을 result라는 변수에 끌어내서 활용하는 바람에
Optional<Member>
를 표시할 수 밖에 없는데 이렇게 하는걸 추천하지 않는다고 하셔서 다음과 같이 구현했다.
회원 조회 메소드는 memberRepository
의 메소드를 활용하면 추가 설명할 내용없이 간단하게 구현할 수 있다.
회원 서비스 테스트
직전에 다룬 회원 서비스 개발에서 구현한 내용을 테스트 케이스를 활용해 테스트 해보자!
예전에 테스트 케이스를 다룰 때처럼 구현해도 당장은 문제가 없지만 강의에서 언급된 내용을 같이 설명하면
일단 @BeforeEach, beforeEach()가 추가됐다.
테스트 케이스에서 돌리든 다른 로직에서 돌리든 MemoryMemberRepository
은 같은 것을 써야하므로
MemberService.java
에는 생성자가, MemberServiceTest.java
에는 다음과 같은 내용이 들어간다.
// MemberService.java
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
// MemberServiceTest.java
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
그리고 MemberService.java
의 테스트 케이스이기 때문에 join()을 테스트하기 위해
MemberServiceTest.java
에서 join()을 추가해도 되지만 내부적인 테스트를 위해서 사용하는 것이므로
굳이 join()으로 쓰지 않고 회원가입()으로 이름을 바꿔서 설정해도 실제로 문제가 없다고 한다.
내용을 보면 주석으로 given
, when
, then
으로 나눴는데
대부분 로직은 위와 같이 3단계로 나눠서 구현이 대부분 가능하기도 하고 명시를 해놓고 구현하는게 편리해
사용했다고 보면 되겠다.
MemberService.java
의 valilast_modified_atDuplicateMember()를 테스트하기 위한 로직은 회원가입()이 아닌
중복_회원_예외()에 구현했다고 보면 된다.
try ~ catch로 이루어진 주석 부분은 주석을 지워도 정상적으로 동작하지만 편리한 문법이 있다고 하셔서
다음과 같이 assertThrows()와 assertThat()을 활용해 마무리했다.
마지막으로 @AfterEach는 이전에도 언급한 것처럼 메소드 실행마다 초기화해주는 역할이며
MemoryMemberRepository
가 필드에 추가된 이유가 @AfterEach때문이라고 보면 된다.
전체적인 코드는 다음과 같다.
댓글남기기