스프링 입문 Section3. 회원 관리 예제 - 백엔드 개발

Bae YuSeon·2024년 4월 11일
1

spring스터디

목록 보기
3/15
post-thumbnail

1. 비즈니스 요구사항 정리

👩‍💼회원 관리 예제를 만들기 위해 비즈니스 요구사항을 정리해야 한다.

  • 📰데이터: 회원 ID, 이름
  • ⚙️기능: 회원을 등록하고 조회
  • 아직 데이터 저장소 DB가 선정되지 않았다 (가상의 시나리오)

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

일반적인 웹 애플리케이션 계층 구조는 컨트롤러, 서비스, 리포지토리, 도메인, DB로 구성

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

클래스 의존 관계

  • 아직 데이터 저장소가 선정되지 않아서, 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계
    • 향후 구체적 기술을 선정하고 바꿔 끼울 수 있게 회원 리포지토리 인터페이스를 만들고 구현체를 메모리 구현체로 만든다.
  • 데이터 저장소는 RDB, NoSQL 등등 다양한 저장소를 고민중인 상황으로 가정
  • 개발을 진행하기 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용

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

회원 객체 만들기
회원 도메인을 만들기 위해 src/main/java/hellospring 폴더 안에 domain이라는 새로운 패키지를 추가하고, 그 안에 Member라는 클래스를 만든다.

package hello.hellospring.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;
    }
}

데이터로 회원 ID와 이름이 있어야 하므로, 회원 ID와 이름에 대한 getter와 setter도 만든다.

  • id: 회원의 고유 식별자를 나타내는 Long 타입의 필드
  • name: 회원의 이름을 나타내는 String 타입의 필드

회원 리포지토리 인터페이스
방금 만든 회원 객체를 저장할 리포지토리를 만든다. src/main/java/hellospring 폴더 안에 repository라는 새로운 패키지를 만들고, 그 안에 MemberRepository라는 인터페이스를 만든다. 인터페이스는 실제 데이터베이스에 접근하여 회원 데이터를 CRUD(Create, Read, Update, Delete)하는 기능을 정의한다. 여기서는 간단하게 회원을 저장하고, 조회하는 기능만 정의한다.

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();
}
  • Member save(Member member);: 주어진 회원 객체를 저장소에 저장. 새로운 회원을 추가할 때 사용되며, 저장된 회원 객체를 반환
  • Optional<Member> findById(Long id);: 주어진 고유 식별자(ID)에 해당하는 회원을 찾아 반환. 만약 해당 ID에 해당하는 회원이 존재하지 않으면 Optional.empty()를 반환.
  • Optional<Member> findByName(String name);: 주어진 이름에 해당하는 회원을 찾아 반환. 만약 해당 이름에 해당하는 회원이 존재하지 않으면 Optional.empty()를 반환
  • List<Member> findAll();: 현재 저장소에 저장된 모든 회원의 리스트를 반환

회원 리포지토리 메모리 구현체
구현체를 만들기 위해 src/main/java/hellospring/repository 패키지 안에 MemoryMemberRepository 라는 새로운 파일을 만든다. 이 파일에 MemberRepository 인터페이스를 구현한다.

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

import java.util.*;

public class MemoryMemberRepository implements MemberRepository {

    //동시성 문제 고려 X
    // 실무에서는 ConcurrentHashMap, AtomicLong 사용 고려
    private static Map<Long, Member> store = new HashMap<>();
    private static long sequence = 0L;

    @Override
    public Member save(Member 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();
    }

    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }
}
  • public class MemoryMemberRepository implements MemberRepository: MemberRepository 인터페이스를 구현하기 위해 implemnets 메서드를 생성
  • save(Member member): 전달된 회원 객체를 메모리에 저장하고, 회원 객체에 고유한 식별자(ID)를 부여한 후 저장된 회원 객체를 반환.
  • findById(Long id): 주어진 고유 식별자(ID)에 해당하는 회원을 메모리에서 찾아 반환. 만약 해당 ID에 해당하는 회원이 존재하지 않으면 Optional.empty()를 반환
  • findByName(String name): 주어진 이름에 해당하는 회원을 메모리에서 찾아 반환. 여러 회원이 같은 이름을 가질 수 있으므로 findAny()를 사용하여 임의의 회원을 반환
  • findAll(): 현재 메모리에 저장된 모든 회원을 리스트 형태로 반환
  • 간단한 실습이여서 동시성 문제를 고려하지 않고 단순히 HashMap을 사용하여 회원 데이터를 저장
    • 실무에서는 ConcurrentHashMap, AtomicLong 사용 고려

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

📕개발한 기능을 실행해서 테스트 할 때
1. 자바의 main 메서드를 통해 실행
2. 웹 애플리케이션의 컨트롤러를 통해서 해당 기능 실행
⇒이런 방법은 준비하고 실행하는데 오래 걸리고, 반복 실행하기 어렵고 여러 테스트를 한번에 실행하기 어렵다는 단점!
⇒ 자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제 해결

MemoryMemberRepository가 정삭적으로 동작하는 지 확인하기 위해 테스트 케이스를 작성할 것이다.
src/test/java 밑에 똑같이 repository라는 패키지를 만들고, MemoryMemberRepositoryTest 라는 클래스 파일을 만든다.

회원 리포지토리 메모리 구현체 테스트
MemoryMemberRepository에서 정의한 각각의 기능에 대한 테스트 케이스를 작성해야 한다.

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.assertj.core.api.Assertions.*;
import java.util.Optional;


class MemoryMemberRepositoryTest {
    MemoryMemberRepository repository = new MemoryMemberRepository();

    @Test
    public void save() {
        //given
        Member member = new Member();
        member.setName("spring");
        //when
        repository.save(member);
        //then
        Member result = repository.findById(member.getId()).get();
        //System.out.println("result" + (result == member));
        //위 코드로 확인할 수 있지만 매번 출력할 수 없으므로 Assertions 사용
        assertThat(result).isEqualTo(member);
    }

    @Test
    public void findByName() {
        //회원 1번
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);
        //회원 2번
        Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2);

        //when
        Member result = repository.findByName("spring1").get();
        //then
        assertThat(result).isEqualTo(member1);
    }

    @Test
    public void findAll() {
        //given
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);

        Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2);

        //when
        List<Member> result = repository.findAll();
        //then
        assertThat(result.size()).isEqualTo(2);
    }
}

@Test 메서드

  • save()

    Member member = new Member();
     member.setName("spring");
    • 테스트를 위해 새 회원 객체를 생성하고 이름을 "spring"으로 설정
    repository.save(member);
    • 회원 객체를 저장소에 저장
    Member result = repository.findById(member.getId()).get();
     assertThat(result).isEqualTo(member);
    • 저장소에서 ID로 회원을 찾아 가져오고, result 변수를 이용해 저장한 회원 객체와 조회한 회원 객체가 동일한지 확인. (동일하지 않으면 테스트 실패)

  • findByName()
          //회원 1번
          Member member1 = new Member();
          member1.setName("spring1");
          repository.save(member1);
          //회원 2번
          Member member2 = new Member();
          member2.setName("spring2");
          repository.save(member2);
    • 테스트를 위해 두 회원 객체를 생성, 저장.
      (회원 1은 "spring1", 회원 2는 "spring2")
      Member result = repository.findByName("spring1").get();
    • 이름이 "spring1"으로 저장된 회원을 조회
      assertThat(result).isEqualTo(member1);
    • result 변수를 이용하여 조회한 회원이 member1와 동일한지 확인 (동일하지 않으면 테스트 실패)

  • findAll()

          Member member1 = new Member();
          member1.setName("spring1");
          repository.save(member1);
    
          Member member2 = new Member();
          member2.setName("spring2");
          repository.save(member2);
    • 두 회원 객체를 생성하고 저장소에 저장
    List<Member> result = repository.findAll();
    • 저장소에 저장된 모든 회원을 조회
    assertThat(result.size()).isEqualTo(2);
    • 저장된 회원의 수가 2명인지 확인 (2명이 아니면 테스트가 실패)

Test 코드를 각각 개별적으로 하나씩 실행할 때는 문제 없이 테스트가 가능했지만, 전체 클래스에 Test를 실행하면 에러가 뜬다.

모든 테스트는 순서랑 상관없이 메소드별로 다 따로 동작하도록 설계를 해야하는데 현재 코드에서는 findByName()과 findAll() 테스트 케이스에서 똑같은 member1과 member2 객체가 생성되어 서로 영향을 주는 상황이 발생했기 때문이다!

이를 해결하기 위해 각 테스트가 실행된 후에 저장소를 초기화하는 메서드 @AfterEach 메서드를 추가한다.

MemoryMemberRepositoryTest 파일에

import org.junit.jupiter.api.AfterEach;

 @AfterEach
    public void afterEach() {
        repository.clearStore();
    }

메서드를 추가하고

MemoryMemberRepository에

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

clearStore() 메서드를 추가하여 store 맵을 초기화하는 작업을 수행하도록 해야 한다.

모든 테스트에 성공했다!

4. 회원 서비스 개발

Extract method 단축키: Ctrl + Alt + m

회원 서비스는 회원 리포지토리랑 도메인을 활용해서 실제 비즈니스 로직을 작성해야 한다.
src/main/java/hellospring 폴더 안에 service라는 패키지를 만들고 그 안에 MemberService라는 클래스를 만든다.

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<Member> result = memberRepository.findByName(member.getName());
//        result.ifPresent( m -> {
//            throw new IllegalStateException("이미 존재하는 회원입니다");
//        });

        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);
    }
}
  • private final MemberRepository memberRepository = new MemoryMemberRepository();

    • MemberRepository 인터페이스를 구현한 MemoryMemberRepository 클래스의 인스턴스를 필드로 가지고 있다.
    • 이 필드는 회원 데이터에 대한 저장, 검색 등을 처리하는 데 사용된다.
  • join(Member member): 회원 가입 기능 제공

    • 중복 회원 검증: validateDuplicateMember(member) 메서드를 호출하여 중복 회원을 검증.
    • 저장: 중복 회원 검증에 문제가 없으면 memberRepository.save(member)를 호출하여 회원을 저장소에 저장
    • 회원의 ID를 반환
  • validateDuplicateMember(Member member)

    • 회원 객체의 이름을 사용하여 중복 회원이 있는지 검증.
    • memberRepository.findByName(member.getName())를 호출하여 동일한 이름을 가진 회원이 있는지 확인.
    • 이 메서드는 Optional<Member> 타입의 객체를 반환. 만약 저장소에 해당 이름을 가진 회원이 존재하면 Optional 객체는 그 회원을 포함
    • 반환된 Optional<Member> 객체가 값(회원)을 포함하고 있으면 ifPresent() 메서드의 람다 표현식 ifPresent(m -> { ... })이 실행
    • 중복 회원이 존재하면 람다 표현식 내부에서 IllegalStateException을 발생시켜 회원 가입 중단
  • findMembers()

    • 전체 회원을 조회하여 리스트로 반환하는 기능.
    • memberRepository.findAll()를 호출하여 저장소에서 모든 회원을 조회하고 반환.
  • findOne(Long memberId)

    • 주어진 ID에 해당하는 회원을 조회하는 기능.
    • memberRepository.findById(memberI이d)를 호출하여 회원을 조회하고, 반환된 Optional<Member> 객체를 반환.

5. 회원 서비스 테스트

이제 그동안 작성한 회원 코드를 테스트하기 위해 테스트 코드를 작성해야 한다.

테스트 생성 단축키: Ctrl + Shift + T

테스트 생성 단축키를 사용하면 보다 빠르게 Test 코드를 작성할 수 있다.

이렇게 하면 src/test/java/service 밑에 자동으로 MemberServiceTest.java 파일의 껍데기가 만들어진다.

package hello.hellospring.service;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {

    @Test
    void join() {
    }

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

Test 코드는 실제로 빌드할 때 포함되지 않아 한글로 작성해도 무방하다.

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;
import static org.assertj.core.api.Assertions.*;

class MemberServiceTest {

    MemberService memberService;
    MemoryMemberRepository memberRepository;

    @BeforeEach
    public void beforeEach() {
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository);
    }

    @AfterEach
    public void afterEach() {
        memberRepository.clearStore();
    }

    @Test
    void join() {
        //give
        Member member = new Member();
        member.setName("hello");

        //when
        Long saveId = memberService.join(member);

        //then
        Member findMember = memberRepository.findById(saveId).get();
        assertEquals(member.getName(), findMember.getName());

    }

    @Test
    public void 중복_회원_예외() throws Exception {
        //given
        Member member1 = new Member();
        member1.setName("spring");
        Member member2 = new Member();
        member2.setName("spring");

        //when
        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class,
                () -> memberService.join(member2));//예외가 발생해야 한다.
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }

필드 선언

  • MemberService memberService;
    • MemberService 객체를 저장할 필드
    • 각 테스트에서 사용할 MemberService 객체를 저장
  • MemoryMemberRepository memberRepository;
    • MemoryMemberRepository 객체를 저장할 필드
    • 각 테스트에서 사용할 MemoryMemberRepository 객체를 저장\

@Test 어노테이션

  • join 테스트 메서드
    • Given: 테스트에 필요한 회원 객체를 생성하고, 이름을 "hello"로 설정
      • Member member = new Member();: 새로운 회원 객체를 생성.
      • member.setName("hello");: 회원 객체의 이름을 "hello"로 설정
    • When: 회원 가입을 수행.
      • Long saveId = memberService.join(member);: memberService.join(member)를 호출하여 회원을 가입시키고, 가입된 회원의 ID를 반환.
    • Then: 가입된 회원을 찾아서 이름이 일치하는지 확인.
      • Member findMember = memberRepository.findById(saveId).get();: saveId를 사용하여 가입된 회원 찾기.
      • assertEquals(member.getName(), findMember.getName());: 가입된 회원의 이름이 입력한 회원의 이름과 일치하는지 확인
  • 중복_회원_예외 테스트 메서드
    • Given: 동일한 이름을 가진 두 회원 객체를 생성
      • Member member1 = new Member();: 첫 번째 회원 객체 생성.
      • member1.setName("spring");: 첫 번째 회원 객체의 이름을 "spring"으로 설정.
      • Member member2 = new Member();: 두 번째 회원 객체 생성.
      • member2.setName("spring");: 두 번째 회원 객체의 이름을 "spring"으로 설정
    • When: 첫 번째 회원 객체를 회원 가입시키고, 두 번째 회원 객체를 회원 가입시키려고 시도
      • memberService.join(member1);: 첫 번째 회원 객체를 회원 가입
      • IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));: 두 번째 회원 객체를 회원 가입시키려 할 때 예외가 발생해야 함
    • Then: 예외 메시지가 예상한 메시지("이미 존재하는 회원입니다.")와 일치하는지 확인
      • assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");: 발생한 예외의 메시지가 예상한 메시지와 일치하는지 확인

@AfterEach 메서드

  • afterEach()
    • 각 테스트가 실행된 후에 실행되는 메서드
    • memberRepository의 store를 초기화해서 각 테스트가 독립적으로 실행 가능
      • memberRepository.clearStore(): MemoryMemberRepository의 store를 초기화하는 메서드

@BeforeEach 메서드

  • beforeEach()
    • 각 테스트가 실행되기 전에 실행되는 메서드
    • memberRepository와 memberService 객체를 초기화
      • memberRepository = new MemoryMemberRepository(): MemoryMemberRepository 객체를 새로 생성
      • memberService = new MemberService(memberRepository): 생성된 MemoryMemberRepository 객체를 주입하여 MemberService 객체를 생성

src/main/java/hello.spring/service/MemberService 코드를

    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

로 수정해 줘야 한다.

각 테스트 케이스가 beforeEach()에서 독립적으로 시작할 때마다 새로운 MemoryMemberRepository와 MemberService 인스턴스를 생성하고, afterEach()에서 MemoryMemberRepository를 초기화하여 각 테스트가 서로 영향을 받지 않도록 한다.

0개의 댓글

관련 채용 정보