- 서버/springboot
회원관리예제
더모어더베러
2024. 6. 8. 00:48
아래와 같은 구성으로 회원관리 예제에 대해 정리하겠습니다.
- 비즈니스 요구사항 정리
- 회원 도메인과 리포지토리 만들기 (회원 도메인 객체를 저장하고 불러올수 있는 리포지토리)
- 회원 리포지토리 테스트 케이스 작성
- 회원 서비스 개발
- 회원 서비스 테스트
여기서 테스트는 junit을 사용하여 작성 합니다.
비즈니스 요구사항 정리
비즈니스 요구사항은 아주 간단한 데이터로 해보겠습니다.
- 데이터 : 회원ID, 이름
- 기능 : 회원등록, 조회
- 아직 데이터 저장소가 선정되지 않음(가상의 시나리오) >> 개발자가 개발은 해야 하는데 db는 아직 안정해진 상황. 일반적인 db로 할지, 관계형 db로 할지 nosql로 할지 정해지지 않은 상황.
- 컨트롤러 : 웹 MVC의 컨트롤러 역할. API 만들기 등
- 서비스 : 핵심 비즈니스 로직 구현. ex) 회원 중복 가입 방지 같은 로직들이 서비스에 들어감
- 도메인 : 비즈니스 도메인 객체. ex) 회원, 주문, 쿠폰 등 주로 데이터베이스에 저장하고 관리
- 리포지토리 : 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리
- 데이터 저장소가 선정 되지 않았다는 환경이기 때문에 우선 인터페이스로 만들어서 구현 클래스를 변경 할수 있도록함. 회원 저장하는 리포지토리를 인터페이스로 만듬. memory 기반의 단순하게 넣다 뺐다 할수 있는 구현체 Memory MemberRepository를 임시로 만들어 놓고 나중에 개발후에 바꿔 끼는 식으로 함. 구현체를 바꿔 끼우려면 인터페이스가 필요.
회원 도메인과 리포지토리 만들기
회원 도메인(객체)를 만들어줍니다.
Member
public class Member {
// 회원정보 클래스
private Long id; // 고객이 정하는 id가 아니라 시스템이 정하는 식별자
private String name;
// getter, setter가 좋냐 안좋냐라는 논쟁이 있는데 이 예제에서는 그냥 간단히 알아보기 위해 getter, 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;
}
}
리포지토리를 만들어줍니다.
MemberRepository
public interface MemberRepository {
// 저장소에 저장하는 Repository
Member save(Member member); // 회원을 저장하면 저장된 회원이 반환되도록 설계할거임
Optional<Member> findById(Long id); // Member 클래스의 id데이터로 회원을 찾는 기능. null로 응답 받을때 null을 Optional로 감싸서 처리 하는것
Optional<Member> findByName(String name); // Member 클래스의 name으로 회원 찾기
List<Member> findAll(); // 지금까지 저장된 모든 회원 정보를 반환
}
리포지토리의 구현체를 만들어 줍니다.
MemoryMemberRepository
public class MemoryMemberRepository implements MemberRepository{
// db 저장소가 없기 때문에 여기에 임시로 저장하기 위한 변수 store
// 실무에서는 공유되는 변수는 동시성 문제가 있을수 있어서 ConcurrentHashMap을 사용해야 하지만 여기선 예제니까 그냥 함
private static Map<Long, Member> store = new HashMap<>();
// sequence는 0,1,2 처럼 식별자로 사용
// 실무에서는 동시성 문제로 어텀롱 등을 해야 하는데 여기선 그냥 심플하게 쓰기 위해 씀
private static long sequence = 0L;
@Override
public Member save(Member member) {
member.setId(++sequence); // 여기서 sequence는 자체적으로 +1 해서 member에 넣어준다
store.put(member.getId(), member); // store에 저장후 member를 리턴
return member;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id)); // store.get(id) 얘가 null일수도 있으니까 그럴경우를 대비해 nullable로 감싸서 클라이언트에서 처리가 가능하도록 한다
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name)) // Map<Long, Member> store의 values(member) 를 불러와서 주입된 파라미터 name과 같을때만 findAny()를 실행. findAny()가 Optional임. 모든 member 속성 조건 확인해봣는데 없으면 Optional에 null포함해서 리턴
.findAny();
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values()); // 실무에서는 map보단 list를 많이 씀. map을 list로 변환 하기 위해 store.values(member)를 넣어 준다.
}
}
회원 리포지토리 테스트 케이스 작성
그 다음으론 작성해놓은 코드에 대해 회원 리포지토리 테스트 케이스 작성을 해보겠습니다.
현재 내가 작성한 코드가 잘 동작하는지를 검증하는 것으로 코드로 코드를 검증하게 됩니다.
자바는 JUnit이라는 프레임워크로 테스트를 실행하게 됩니다.
아래는 회원 리포지토리 메모리 구현체를 테스트 할수 있는 코드입니다.
테스트 코드는 main 폴더가 아닌 test 폴더에 파일을 만들어 사용합니다.
MemoryMemberRepositoryTest
package com.group.inf.repository;
import com.group.inf.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.assertThat;
// class 레벨로 돌려서 모든 @Test들을 한번에 체크가 가능하다. 또는 패키지 폴더에서 run도 가능하다
// 모든 테스트들은 서로에게 의존관계 상관없이 독립적으로 테스트 되야 하기 때문에 테스트 순서는 보장이 안된다. 그래서 테스트가 끝날때마다 AfterEach 초기화를 해줘야 한다.
public class MemoryMemberRepositoryTest {
// db에 저장하는데 밀접한 관련이 있는 Repository 구현체를 테스트
// 코드를 먼저 만들기 전에 테스트 코드를 먼저 만들어 놓는것을 tdd 테스트 주도 개발이라고 한다.
MemoryMemberRepository repository = new MemoryMemberRepository();
// 각 테스트 메소드가 끝날때마다 동작하는 어노테이션 AfterEach
@AfterEach
public void afterEach() {
repository.clearStore(); // 각 테스트가 끝날때마다 repository를 비워줘야 테스트가 정상적으로 진행이 가능해진다.
}
@Test // junit의 Test 어노테이션을 추가 하면 save() 메소드를 실행 할수 있다.
public void save() { // 저장이 잘되는지 테스트
Member member = new Member();
member.setName("jdh");
repository.save(member);
Member result = repository.findById(member.getId()).get(); // Optional은 get()으로 데이터를 꺼낼수가 있다. 사실 get()으로 바로 꺼내는게 좋은 방식은 아니나 예제이기 때문에 간단히 사용하기 위해 쓴다.
// 내가 repository.save로 저장한 member와 저장후 repository.findById()로 불러온 member가 똑같으면 저장이 잘되었다는뜻. 그렇게 테스트 하기
// System.out.println("result = " + (result == member)); // 이렇게 해도 결과는 확인되지만 테스트는 이렇게 진행하지 않는다. Assertions를 사용한다.
assertThat(result).isEqualTo(member);
}
@Test
public void findByName() { // name으로 회원정보를 불러오는것이 잘 동작 하는지 테스트
Member member1 = new Member();
member1.setName("jdh1");
repository.save(member1);
Member member2 = new Member();
member2.setName("jdh2");
repository.save(member2);
Member result = repository.findByName("jdh1").get(); // get()으로 Optional을 까고 안에 있는 객체(Member)를 받음
// Member result = repository.findByName("jdh2").get(); // jdh2를 찾고 member1과 result를 assertThat으로 비교 하면 테스트 실패함
assertThat(result).isEqualTo(member1);
}
@Test
public void findAll() {
Member member1 = new Member();
member1.setName("jdh1");
repository.save(member1);
Member member2 = new Member();
member2.setName("jdh2");
repository.save(member2);
List<Member> result = repository.findAll();
assertThat(result.size()).isEqualTo(2); // 간단히 비교 할때 repository에 save를 2개 했으니까 result 사이즈는 2일것이다라고 테스트
}
}
이 Test 코드를 run하여 테스트가 정상 동작 했는지 확인이 가능합니다.
테스트한 findAll(), findByName(), save() 테스트코드가 정상 작동한것을 확인할수 있습니다.
회원서비스 개발
회원 도메인과 리포지토리를 활용하여 실제 비즈니스 로직을 작성하는 부분입니다.
서비스 폴더를 만들고 클래스를 만들어 줍니다.
MemberService
package com.group.inf.service;
import com.group.inf.domain.Member;
import com.group.inf.repository.MemberRepository;
import com.group.inf.repository.MemoryMemberRepository;
import java.util.List;
import java.util.Optional;
public class MemberService {
// 리포지토리는 데이터를 넣어다 뺏다 하는 느낌의 메소드가 있고 (ex save, find), Service의 메소드는 비즈니스의 느낌에 가깝다
// 서비스 클래스는 비즈니스에 가까운 네이밍을 사용해야 한다. (join)
// 회원 서비스를 만드려면 리포지토리가 필요
private final MemberRepository memberRepository = new MemoryMemberRepository();
/**
* 회원가입
*/
public Long join(Member member) {
validateDuplicateMember(member); // 중복 회원이름이면 가입 안되도록
memberRepository.save(member); // save 해주면 데이터가 저장되므로 회원가입
return member.getId();
}
// 중복 회원일때 아래의 IllegalStateException이 발생 하는지 테스트코드 짜야됨
private void validateDuplicateMember(Member member) {
// 같은 이름이 있는 중복 회원은 가입이 안되도록. 실제로는 이름이 같을수는 있지만 조건을 추가하기 위해 중복이름 x
Optional<Member> result = memberRepository.findByName(member.getName());
// result.orElseGet(); // result.get()으로 Member를 바로 꺼낼수도 있지만 권장하진 않음. orElseGet()을 사용하여 객체가 있으면 꺼내고 null이라면 메소드를 실행시켜 디폴트 값을 넣어서 꺼내도록 사용
result.ifPresent(m -> { // ifPresent(optional기능) : 만약 null이 아닌 값이 있으면 그 값을 m(Member)으로 주입되도록 작성. Optional<Member>이기 때문에 member
throw new IllegalStateException("이미 존재하는 회원입니다."); // 예외 발생시키기
});
}
/**
* 전체 회원 조회
*/
public List<Member> findMembers() {
return memberRepository.findAll();
}
/**
* 회원 조회
*/
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
그 다음으로 중
회원 서비스 테스트 11:23