[Spring] 회원 관리 예제

!·2022년 7월 24일
0

스프링

목록 보기
2/2

비즈니스 요구 사항 정리

  • 데이터 : 회원ID, 이름
  • 기능 : 회원 등록(중복된 ID는 허용하지 않는다), 조회
  • 아직 데이터 저장소가 선정되지 않음 -> 인터페이스로 구현(어느 저장소이든간에 상속을 통해 구체화할 수 있도록)

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

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

클래스 의존관계

  • 아직 구체적인 데이터 저장소가 선정되있지 않았기때문에 우선 인터페이스로 구현하였으며, 이번 튜토리얼에서는 메모리에 저장소를 이용한다.
  • 데이터 저장소는 RDB, NoSQL 등등 다양한 저장소를 고민중인 것으로 가정한다.

패키지 관계 정리

  • src 파일 내부의 main과 test파일로 나뉘며, 각 웹 애플리케이션 계층 구조애 따라 패키지를 만든 뒤, 그 하위에 소스파일 집어 넣는다.
  • test와 main은 별개로 실행이 되며, 특히 test에서는 함수를 독립적으로 시행할 수도 있다.
  • 또한, test는 빌드에 포함되지 않으므로 구별하기 쉽도록 한글 함수명을 사용할 수 있다.

Domain

package Hello.HelloSpring.Domain;

public class Member {

    private String name;
    private Long id;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }
}
  • 회원 객체가 존재하는 도메인 영역으로, 회원의 멤버를 idname 으로 선언하였으며, gettersetter 함수를 구현하였다.
  • 이때 getter, setter 함수는 단축키 command + N 을 통해 쉽게 만들 수 있다.

Repository

  • 인터페이스(MemberRepository)
package Hello.HelloSpring.Repository;
import Hello.HelloSpring.Domain.Member;
import java.util.Optional;
import java.util.List;
public interface MemberRepository {
    Member save(Member member);

    Optional<Member> findById(Long id);

    Optional<Member> findByName(String name);

    List<Member> findAll();
}
  • 클래스(MemoryMemberRepository)
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);
        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<Member>(store.values());
    }

    public void clearStore(){
        store.clear();
    }
}
  • HashMap 자료구조를 통해 메모리에 회원들을 저장하였으며, static 변수를 선언함으로써 생성자 필요없이 저장소에 클래스 레벨로 접근할 수 있도록 하였다.
  • save() 함수에서는 단순히 멤버를 저장소에 추가하는 함수로 구현하였다. (서비스 레벨이 아닌, 단순히 저장소에 멤버를 추가하는 것)
  • findById(), findByName() 함수는 각각 id와, name을 통해 회원을 찾는 함수이다. 이때 존재하지 않는다면 null이 나올 수 있기때문에 Optional 로 한 번 감싼다.
  • findAll() 함수는 store에 저장된 멤버들의 들을 ArrayList 형식으로 리턴한다.

Repository TEST

package Hello.HelloSpring.Repository;

import Hello.HelloSpring.Domain.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import java.util.List;

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

class MemoryMemberRepositoryTest {
    MemoryMemberRepository repository = new MemoryMemberRepository();

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

    @Test
    public void save()
    {
        Member member = new Member();
        member.setName("spring");
        repository.save(member);
        Member result = repository.findById(member.getId()).get();
        System.out.println("result = " + (result==member));
        //Assertions.assertEquals(member,result);
        assertThat(member).isEqualTo(result);
    }

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

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

        Member result = repository.findByName(member1.getName()).get();
        assertThat(member1).isEqualTo(result);
    }

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

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

        Member member3 = new Member();
        member3.setName("Spring3");
        repository.save(member3);

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

        assertThat(result.size()).isEqualTo( 3);

    }
}

작성한 함수를 테스트하는데 있어서 main 메소드를 통해 실행해 확인해보거나, 웹 애플리케이션 컨트롤러를 통해 확인하면, 시간이 오래걸리고 생산성이 떨어진다. 자바의 프레임워크중 JUnit이라는 테스트를 실행해서 이러한 문제를 해결한다.

  • afterEach 함수는 테스트 파일내의 하나의 함수가 종료될때마다 실행되는 함수로, 각 테스트 함수들의 독립성을 보장하기 위해서 사용한다.
  • 테스트 함수 작성시에는 주로 Assertions.assertThat().isEqualTO() 함수를 이용한다.
  • 주로 import문을 빠르게 추가 선언하는 방법은 단축키 option + Enter 를 통해 할 수 있다.
  • 일반적으로 TEST 함수를 작성시에는 한글로 함수명을 정의하며, given, when, then 세 파트를 통해 정의한다. (주석을 통해 표현)

Service

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;
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    } // MemberService와 MemberServiceTest에서의 같은 인스턴스를 사용하기 위해. DI(Dependency Injection)

    public Long join(Member member)
    {
        validateDuplicateMember(member); //ctrl + t
        // 회원 중복 검사
        memberRepository.save(member);
        return member.getId();
    }

    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });
    }

    public List<Member> findMembers(){
        return memberRepository.findAll();
    }

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

주로 서비스레벨에서의 기능을 구현한다. 단순히 회원 저장소에 회원을 넣는 것에다, 중복id를 허용하지 않는 기능을 추가하며, 모든 회원 검색, 회원 한명 검색등의 기능 또한 서비스 레벨에서 구현한다.

  • 회원 중복 검사시에 함수를 따로 빼내고 있다. 해당 단축키는 ctrl + t로 가능하며, 중복 회원이 존재시에 IllegalStateException 메시지를 던진다.

  • 테스트 코드는 원래 코드에서 단축키 ctrl + t 를 통해 접근 할수 있다.


Service TEST 및 의존성 주입(DI)

package Hello.HelloSpring.Service;

import Hello.HelloSpring.Domain.Member;
import Hello.HelloSpring.Repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.List;

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

class MemberServiceTest { // command + shift + t

    MemberService memberService;
    MemoryMemberRepository memberRepository;

    @BeforeEach
    public void beforeEach()
    {
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository); // 의존성 주입. 엄연히 다른 인스턴스이므로...
    }

    @AfterEach
    void afterEach()
    {
        memberRepository.clearStore();
    }
    @Test
    void 회원가입() { // 빌드에 포함되지 않기때문.
        //given
        Member member = new Member();
        member.setName("hello");

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

        //then
        Member findMember = memberService.findOne(saveId).get();
        Assertions.assertThat(member.getName()).isEqualTo(findMember.getName());
    }
    @Test
    public void 중복_회원_예외()
    {
        //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));
        //command + option + v
        Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");


        /*
        try {
            memberService.join(member2);
            fail();
        }catch (IllegalStateException e)
        {
            Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.123123");
        }*/

        //then
    }

    @Test
    void findMembers() {
        //given
        Member member = new Member();
        member.setName("spring1");
        memberService.join(member);

        Member member2 = new Member();
        member2.setName("spring2");
        memberService.join(member2);

        Member member3 = new Member();
        member3.setName("spring3");
        memberService.join(member3);

        //then
        List<Member> findMemberList = memberService.findMembers();
        Assertions.assertThat(findMemberList.size()).isEqualTo(3);

    }

    @Test
    void findOne() {
        //given
        Member member = new Member();
        memberService.join(member);

        //when
        Member findMember = memberService.findOne(member.getId()).get();
        Assertions.assertThat(findMember).isEqualTo(member);


    }
}

해당 테스트 코드에는 의존성 주입 DI의 개념이 들어가 있다.
왜 이 테스트 코드에는 DI가 필요할까?
잘 생각해보자. 테스트 코드에 회원이 저장되는 저장소와, 실제 Service 코드에서 회원이 저장되는 저장소가 같은가?

`public class MemberService {
      private final MemberRepository memberRepository =
                    new MemoryMemberRepository();

TEST 코드에서 MemberService에 속한 기능(함수)들을 검증하기 위해서는 반드시 기본 생성자를 통해 생성해야 한다. 또한 TEST 코드에서 MemberRepository 또한 따로 생성해야 했다. 즉 이는, 서로 다른 객체이다. 따라서 이를 위해 다음과 같이 코드를 작성한다.

public class MemberService {
    private final MemberRepository memberRepository;
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    } // MemberService와 MemberServiceTest에서의 같은 인스턴스를 사용하기 위해. DI(Dependency Injection)

MemberService 객체를 생성할 때, 반드시 repository를 인자로 전달해 객체를 생성하도록 하고 있다. 따라서 TEST 시에 동일한 저장소 를 이용할 수 있게 된다.


인텔리제이 단축키
1. Command + N / ctrl + ENTER : getter, setter 함수 생성, 테스트 함수 생성
2. ctrl + T + 메소드 추출 : 함수 추출 후 private 함수로 분리
3. option + ENTER : 패키지 자동 추가(제네릭 등)

profile
개발자 지망생

0개의 댓글