[Spring Introduction] 3. 회원 관리 예제 - 백엔드 개발


[1] 비즈니스 요구사항 정리

데이터: 회원 ID, 이름
기능: 회원 등록/조회
아직 데이터 저장소가 선정되지 않음 (가상 시나리오)

일반적인 웹 애플리케이션 계층 구조

  • 컨트롤러: 웹 MVC의 컨트롤러 역할
  • 서비스: 핵심 비즈니스 로직 구현 (ex. 회원 중복 타입 X)
  • 리포지토리: 데이터베이스에 접근, 도메임 객체를 데이터베이스에 저장하고 관리
  • 도메인: 비즈니스 도메인 객체 (ex. 회원, 주문, 쿠폰 등등 주로 데이터베이스에 저장하고 관리)

  • 아직 DB가 결정되지 않았기 때문에 인터페이스로 해놓고 추후 구현 클래스를 변경할 수 있도록 설계
  • 데이터 저장소는 RDB, NoSQL 등 다양한 저장소를 염두해둠
  • 개발을 진행하기 위해서 초기 개발 단계에선 구현체로 가벼운 메모리 기반 데이터 저장소 사용할 것

[2] 회원 도메인과 리포지토리 만들기

Optional<>: 비어있다면 null이 반환되는데 이때 optional로 감싸 반환되는 것을 선호함

✔️ MemoryMemberRepository.java 주석참조

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

import java.util.*;

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);  // map에 저장시킴

        return member;
    }

    @Override
    public Optional<Member> findById(Long id) { // store에서 꺼내오면 됨
        return Optional.ofNullable(store.get(id));  // null이어도 감쌀 수 있음
        // 감싸서 반환해주면 클라이언트가 뭘 할 수가 있어짐
    }

    @Override
    public Optional<Member> findByName(String name) {
        // 루프를 돌면서 찾으면 반환해주고 없으면 optional에 null을 감싸 반환

        return store.values().stream()
                .filter(member -> member.getName().equals(name))   // 파라미터로 넘어온 이름과 같은지 확인
                .findAny(); // 그리고 찾으면 반환해주는
    }

    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }
}

[3] 회원 리포지토리 테스트 케이스 작성

JUnit이라는 프레임워크로 테스트를 실행해 여러 테스트를 한 번에 실행

✔️ MemoryMemberRepository.java 주석참조

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

import java.util.*;

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);  // map에 저장시킴

        return member;
    }

    @Override
    public Optional<Member> findById(Long id) { // store에서 꺼내오면 됨
        return Optional.ofNullable(store.get(id));  // null이어도 감쌀 수 있음
        // 감싸서 반환해주면 클라이언트가 뭘 할 수가 있어짐
    }

    @Override
    public Optional<Member> findByName(String name) {
        // 루프를 돌면서 찾으면 반환해주고 없으면 optional에 null을 감싸 반환

        return store.values().stream()
                .filter(member -> member.getName().equals(name))   // 파라미터로 넘어온 이름과 같은지 확인
                .findAny(); // 그리고 찾으면 반환해주는
    }

    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }

    public void clearStore() {
        store.clear();
    }

}

이런 에러를 방지하기 위해 Aftereach 메소드 작성
(테스트가 하나 끝나면 데이터를 clean 시켜주도록 함)

모든 테스트는 동작 순서가 보장되지 않기 때문에 순서에 의존해 설계하면 절대 안됨.

@AfterEach: 한 번에 여러 테스트를 실행하면 메모리 DB에 직전 테스트 결과가 남아있을 수 있다. 그럼 그 잔여물 때문에 다음 진행할 테스트에 실패할 수 있다.
@AfterEach를 사용하면 각 테스트가 종료될 때마다 해당 메소드를 실행한다. 이 예제에서는 DB를 비워주었기 때문에 다음 테스트도 성공할 수 있었다.


[4] 회원 서비스 개발

command + option + m: 메소드로 뽑아내기

✔️ MemberService.java 주석참조

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;

import java.util.List;
import java.util.Optional;

public class MemberService {

    private final MemberRepository memberRepository = new MemoryMemberRepository();

    /**
     *     회원 가입
     */
    public Long join(Member member) {
        // 같은 이름을 가진 중복 회원은 가입 X
        validateDuplicateMember(member);
        memberRepository.save(member);

        return member.getId();
    }

    private void validateDuplicateMember(Member member) {
        // optional이기 때문에 ifPresent 이런 값도 사용할 수 있는 것. (null이라면 이런 문을 안써도 됨)
        memberRepository.findByName(member.getName())
            .ifPresent(m -> { // 찾은 값이 존재한다면
                throw new IllegalStateException("이미 존재하는 회원입니다.");
            });
    }

    /**
     *
     * 전체 회원 조회
     */
    public List<Member> findMembers() {
        return memberRepository.findAll();
    }

    public Optional<Member> findOne(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

[5] 회원 서비스 테스트

테스트 메소드 이름은 과감하게 한글로 해도 됨.

그리고 given, when, then을 지켜서 짜는 것을 추천

테스트의 핵심은 예외잡기. 예외상황 발생시 잘 동작하는지를 확인하는 것이 중요함

기존에는 회원 서비스가 메모리 회원 리포지토리를 직접 생성하도록 했으나 회원 리포지토리 소스가 회원 서비스 소스를 DI 가능하도록 변경했다.

@BeforeEach: 각 테스트 실행 전 호출
테스트가 서로 영향받지 않도록 항상 새로운 객체를 생성하고 의존 관계를 새로 맺어줌.


좋은 웹페이지 즐겨찾기