본문 바로가기
BackEnd/Spring

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

by 개발 Blog 2024. 6. 30.

공부 내용을 정리하고 앞으로의 학습에 이해를 돕기 위해 작성합니다.

인프런 스프링 입문 (김영한)

 

비즈니스 요구사항 정리

 

스프링 입문 강의(김영한)

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

스프링 입문 강의(김영한)

  • 데이터는 회원 ID, 이름뿐인 단순한 구조이다.
  • 기능은 회원 조회, 등록을 구현할 것이다. 
  • 아직 데이터 저장소가 선정되지 않아서 인터페이스로 구현한다. (추후에 변경할 수 있도록)

 

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

1. domain이라는 패키지를 만들고 Member 클래스를 생성한다.

package hello.hello_spring.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와 name을 만든다.
  • id는 시스템에서 회원을 구분하기 위한 값이다.
  • name은 그냥 이름이다.

2. repository라는 패키지를 만들고 MemberRepository 인터페이스를 구현한다.

package hello.hello_spring.repository;

import hello.hello_spring.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();
}
  • 4가지 기능을 하는 인터페이스이다.
  • 회원 정보 저장, ID/Name 찾기, 모든 회원 정보 찾기

3. 다음으로는 인터페이스의 구현체인 MemoryMemberRepository 클래스를 만든다.

package hello.hello_spring.repository;

import hello.hello_spring.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);
        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());
    }
}
  • save 할 때 저장할 Map을 구현한다.
    • 실무에서는 동시성 문제가 있을 수 있어 공유되는 변수일 때는 concurrenthashmap을 쓴다고 한다.
  • sequence는 0, 1, 2 같이 키값을 생성해 준다.
    • 실무에서는 동시성 문제가 있을 수 있어 Long 대신 atomiclong 등을 쓴다고 한다.
  • save 메서드에서 id를 세팅하고 map에 저장한다.
  • findById 메서드에서 값을 꺼낼 때 Null이 반환될 가능성이 있기 때문에 Optional로 감싸준다. 
  • findByName 메서드에서는 filter로 조건(파라미터로 넘어온 Name이랑 같은지)에 맞는 값을 필터링한다.
  • 값을 찾으면 반환한다.

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

1. 개발한 기능을 JUnit이라는 프레임워크로 테스트를 할 수 있다.

 -> main 메서드나 웹 애플리케이션의 컨트롤러로 실행하면 너무 오래 걸리고 반복적으로 실행하기가 어렵다.

2. main 폴더가 아닌 test폴더에 패키지를 만든다.

3. Test 할 때는 관례상 이름 뒤에 Test를 붙인다 ex) MemoryMemberRepositoryTest

4. assertThat(member).isEqualTo(result); 로 두 값이 같은지 비교할 수 있다.

5. Shift + F6은 이름을 한 번에 변경할 수 있다.

6. 모든 테스트의 실행 순서는 보장이 안된다.

  -> 한번에 여러 테스트를 실행하면 메모리 DB에 직전 테스트의 결과가 남을 수 있다. -> 다음 테스트에서 오류 발생

  -> 모든 테스트는 순서와 의존관계없이 설계가 되어야 한다.

  -> 테스트가 하나 끝나면 데이터를 clear 해줘야 한다.

  -> 그렇게 하기 위해 MemoryMemberRepository에 clearStore 메서드를 추가한다.

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

-> 그리고 Test 클래스에 @AfterEach를 사용해서 각 테스트가 종료될 때마다 DB에 저장된 데이터를 삭제한다.

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

 

MemberRepositoryTest 전체 코드

package hello.hello_spring.repository;

import hello.hello_spring.domain.Member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import java.util.List;

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

class MemberRepositoryTest {
    MemoryMemberRepository repository = new MemoryMemberRepository();

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

    @Test
    public void save(){
        Member member = new Member();
        member.setName("chan");

        repository.save(member);

        Member result = repository.findById(member.getId()).get();
        assertThat(member).isEqualTo(result);
    };

    @Test
    public void findByName(){
        Member member1 = new Member();
        member1.setName("chan1");
        repository.save(member1);

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

        Member result = repository.findByName("chan1").get();

        assertThat(result).isEqualTo(member1);
    }

    @Test
    public void findByAll(){
        Member member1 = new Member();
        member1.setName("chan1");
        repository.save(member1);

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

        List<Member> result = repository.findAll();

        assertThat(result.size()).isEqualTo(2);
    }
}

 

회원 서비스 개발

1. 회원 서비스는 repository와 domain을 활용해서 실제 비즈니스 로직을 작성해볼 것이다.

2. 회원 가입 메서드를 만드는데 같은 이름이 있는 중복 회원은 안 되는 로직을 작성할 것이다.

private final MemberRepository memberRepository = new MemoryMemberRepository();
public Long join (Member member) {
    //같은 이름이 있는 중복 회원 X
    Optional<Member> result = memberRepository.findByName(member.getName());
    result.ifPresent(m -> {
        throw new IllegalStateException("이미 존재하는 회원입니다.");
    });
  • MemoryMemberRepository의 findByName은 이미 Optional로 감싸져 있기 때문에 
  • memberRepository.findByName(member.getName());의 반환은 Optional이다. 
  • 따라서 Optional을 생략할 수 있다. 
public Long join (Member member) {
    //같은 이름이 있는 중복 회원 X
    memberRepository.findByName(member.getName())
            .ifPresent(m -> {
        throw new IllegalStateException("이미 존재하는 회원입니다.");
    });

 

3. 메서드를 뽑아내는 단축키 command + option + m 

4. Memberrepository는 save, findById, findByName, findAll 등 네이밍이 단순히 저장소에 넣었다 뺐다 하는 느낌이면 

    MemberService는 join, findMembers 등 네이밍이 비즈니스에 가깝다. 

 -> Service 클래스는 비즈니스에 가까운 용어를 써야 한다.

 -> Service는 비즈니스에 의존적으로 설계 / Repository는 단순히 기계적으로 개발스럽게 설계

 

회원 서비스 테스트

1. 테스트 클래스 생성 단축키 : command + shift + T

2. 테스트 작성 문법 Tip

  • given : 준비
  • when : 실행
  • then  : 검증

3. 테스트 메서드는 한글로 작성해도 된다. (배포하지 않기 때문)

4. 테스트는 예외 flow가 더 중요하다.

5. 예외로직

MemberServiceTest

6. MemberServiceTest에서 문제점

MemberServiceTest

- 테스트에서 사용하는 memberRepository는 new로 다른 객체를 사용하는 문제가 있다.

MemberService

- 다른 repository로 테스트하는 문제를 해결하기 위해 같은 인스턴스를 쓰게 바꿀 것이다.

 

7. 해결방법

MemberService

- 이렇게 직접 repository를 생성하는 것이 아니라, 외부에서 의존성을 넣어주도록 바꿔준다.

 

MemberServiceTest

- 그리고 Test에서는 @BeforeEach를 이용하여 테스트 실행할 때마다 각각 MemoryMemberRepository와 Service를 생성한다.

@BeforeEach 흐름

- 각 테스트를 실행하기 전에 @BeforeEach의 MemoryMemberRepository를 만들어서 memberRepository에 넣는다.

- 그리고 MemberService의 생성자에 인자로 memberRepository를 넣는다.

- 이렇게 하면 같은 memberRepository를 사용할 수 있다.

- MemberService 입장에서 직접 new 하지 않고 외부에서 데이터를 넣어준다. 이것을 Dependency Injection(의존성 주입)이라고 한다.

 

마무리

이번 시간에는 비즈니스 요구사항 정리부터 테스트까지 백엔드 개발을 해보았다. 

중간중간 람다식과 스트림 문법은 낯설었지만 새로운 방법을 배우는 즐거움을 느낄 수 있었다.

아직 모르는 것도 많고 부족한 점도 있지만 이는 그만큼 앞으로 배울 것이 많다는 의미이기도 하다.

즉, 성장할 일만 남았다는 것이다. 과거의 나보다 발전하는 내가 되기를 바라며 앞으로도 꾸준히 개발 공부를 이어나갈 것이다.