[2025 스프링부트 스터디] #3주차
섹션 4. 회원 관리 예제 - 백엔드 개발
비즈니스 요구사항 정리
1. 데이터 저장소가 선정되지 않고 개발해야 한다는 가정하에 실습을 진행한다.
2. interface로 구현체를 만들고 메모리에 저장하는 방식으로 진행할 것이다.
회원 도메인과 리포지토리 만들기
Member 도메인 생성.
package wink.spring_boot_study.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;
}
}
Member Repository 생성.
package wink.spring_boot_study.repository;
import java.util.List;
import java.util.Optional;
import wink.spring_boot_study.domain.Member;
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
구현체를 만들기 위한 MemoryMemberRepository 생성.
package wink.spring_boot_study.repository;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import wink.spring_boot_study.domain.Member;
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));
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny(); // findAny는 하나로 찾는 것. 결과가 Optional로 반환 됨. 값이 없으면 Optional에 Null이 포함되서 반환 됨.
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values()); // 자바에서 실무할 때는 루프 돌리기 편한 List를 많이 쓴다.
}
}
요즘에는 null을 그대로 반환하는 대신 Optional로 감싸서 반환을 한다. 이렇게 하면 클라이언트 측에서 뭔가를 해줄 수 있다고 한다. 자바 8부터 들어가 있는 기능이다.
회원 도메인과 리포지토리 만들기
방금 만든 Repository 클래스가 내가 원하는데로 정상적으로 동작할지를 검증하기 위해 테스트 케이스라는 걸 작성한다. 기본적으로 main 메서드를 통해서 실행하거나, 웹 애플리케이션의 컨트롤러를 통해서 DB에 넣어보고 하거나 하면서 해당 기능을 실행하는 방법이 있다. 하지만 이러한 방법은 오래 걸리고, 여러 번 실행하거나 반복 실행하기 어렵다는 단점이 있기 때문에 우리는 JUnit이라는 프레임 워크를 사용한다.
package wink.spring_boot_study.repository;
class MemoryMemberRepositoryTest {
}
네이밍 관례는 뒤에 'Test'를 붙이는 것이다. 딱히 다른 곳에서 쓸 것이 아니기 때문에 Public으로 만들어주지 않아도 된다.
VSCode 기준, 그냥 왼쪽의 디버깅 버튼을 누르면 save를 바 테스트 해볼 수 있다.
테스트 코드는 마치 메인메소드 쓰는 것과 비슷하다.
이런식으로 값이 잘 들어갔는지 비교하며 출력하면서 할 수도 있겠지만, 테스트 케이스가 많을 시 이걸 하나하나 다 봐줄 수는 없기 때문에 Assert라는 것을 사용해야 한다.
Assert를 통해 오류를 감지하면 이렇게 편리하게 검증할 수 있다.
Assertions.assertThat(member).isEqualTo(result);
하지만 org.assertj.core.api를 import해서 쓰면 위와 같이 더 직관적인 assert를 쓸 수 있다.
그리고 윈도우+VSCode 기준으로, 커서를 '.'위치에 둔 후에 Ctrl+. 을 누르면 위처럼 static convert를 해줄 수가 있어, 코드를 좀더 간략화 시킬 수가 있다.
assertThat(member).isEqualTo(result);
주의할 점: 테스트 코드는 메소드의 동작 순서를 보장하지 않는다. 그러므로 각 메소드에서 사용한 데이터는 클리어를 해주어야만 한다. 그렇지 않으면 충돌이 일어날 수 있다.
public class MemoryMemberRepository implements MemberRepository {
...
public void clearStore() {
store.clear();
}
클래스 내부에 클리어 메소드 생성.
@AfterEach
public void afterEach() {
repository.clearStore();
}
어떤 메소드가 끝날 때마다 실행해주는 콜백 메소드 작성.
혼자 개발할 때는 테스트 코드 없이도 잘 되지만, 나중에 협업해서 몇 만, 몇 십만 줄 되는 코드를 다루게 될 때는 테스트 코드가 필수다.
회원 서비스 개발
public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
/**
* 회원 가입
* @param member
* @return
*/
public Long join(Member member) {
// 같은 이름이 있는 중복 회원 X
Optional<Member> result = memberRepository.findByName(member.getName());
result.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
memberRepository.save(member);
return member.getId();
}
}
과거에는 if null, 이런 식으로 코딩 했지만, 지금은 null일 가능성이 있으면 셔널이라는 걸 한 번 감싸서 반호나을 해주고 그 감싼 것 덕분에 if present 라는 걸 쓸 수가 있다.
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
어차피 반환 값이 옵셔널이라면, 이렇게 줄여서 쓸 수도 있다.
참고로, 보통 서비스는 비즈니스에 의존적으로 설계를 하고, 레포지토리 같은 경우에는 약간 더 서비스보다는 기계적으로 개발스러운 용도로 쓰이기 때문에, 그에 맞는 용어들로 네이밍을 해야 한다.
회원 서비스 테스트
VSCode에서 간편하 테스트 코드 만드는 법
1. 클래스 명에 커서를 두고 Ctrl+. 을 누른다.
2. 아래 사진과 같이 목록이 뜨는데, 맨 아래의 Source Actions...를 클릭한다.
3. 그럼 아래 사진 처럼 뜰 것이다. Generate Tests... 를 클릭해주자.
4. 파일 명을 정하고
5. 테스트 메소드를 정하면
6. 짜잔, intellij 부럽지 않게 VSCode에서도 될 거 다 된다~
try catch를 사용한 코드와 assertThrows를 사용한 코드
try {
memberService.join(member2);
fail();
} catch (IllegalStateException e) {
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
그런데, service 코드와 test코드에서 둘 다 new MemoryMemberRepository()를 해주고 있기 때문에 조금 실수하면 서로 다른 내용물을 갖게 되는 문제가 발생할 수 있다. static으로 선언해주었기에 지금은 문제가 없다. 많약 static 선언을 안해주었다면 DB가 여러 개가 되었을 것이다. (현재 스펙/요구 사항 기준, 여기서 말하는 DB는 이해를 돕기 위한 개념일 뿐이다. 그냥 메모리다.)
그러니 아래와 같이 sevice 코드에서는 생성자를 만들어주고 test 코드에서 초기화를 하는 코드를 작성한다.
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
...
public class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
...
이런 방식을 Dependency Injection이라고 한다.