Spring

spring - 회원관리

팅탱팅탱 2024. 3. 3. 04:12

비즈니스 요구사항

데이터: 회원 id, 이름

기능: 회원 등록, 조회

아직 데이터 저장소가 선정되지 않은 가상의 시나리오

아직 데이터 저장소가 선정되지않아, 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계

데이터 저장소는 RDB, NoSQl등 다양한 저장소를 고민중인 상황으로 가정

개발을 진행하기 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용

 

 

우선 이런식으로 멤버 클래스를 작성해주었다 

id와 name의 값을 설정해주고 반환해주는 메서드이고

인터페이스에는 이렇게 회원정보를 저장하고 조회하는 기능을 정의하였다

 

또한 인터페이스를 구현하는 구체적인 클래스인 MemoryMemberRepository를 아래와 같이 정의해주었다.

public class MemoryMemberRepository implements MemberRepository{

    private static Map<Long, Member> store = new HashMap<>();
    //store는 회원정보를 저장하는데 사용되는 hashmap이며 회원의 고유 식별자를 키로 사용하고 해당 회원 정보를 값으로 저장
    private static long sequence = 0L;
    //여기서 이 sequence는 0,1,2 이렇게 키값을 설정해주는것
    @Override
    public Member save(Member member) {
        //멤버 세이브할때 우선 시퀀스값 하나 올려주고
        member.setId(++sequence);
        store.put(member.getId(), member);
        return member;
        //이렇게하면 store에 넣기전에 멤버 id값을 세팅해주고
        //store에 저장함 그럼 맵에 저장이 되겠지
    }

    @Override
    public Optional<Member> findById(Long id) {
        //여기 findbyid는 store에서 추출해야함
        //근데 만약 추출되는 값이 null일수도 있으니까 Optional.ofNullable로 감싸줘야함
        //이렇게 해주면 클라이언트 측에서 처리 가능
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public Optional<Member> findByName(String name) {
        //이름을 찾으려면 람다를 써서 그냥 store에서 돌리면됨
        //람다를 사용하여서 받은 name이 member안에있는 name이랑 같은지 확인 후 찾으면 반환
        //여기서는 findAny에 따로 옵셔널 null값 처리를 안해줘도되는 이유가 이 값이 위에보이는 optional로 처리가 됨

            return store.values().stream()
                    .filter(member -> member.getName().equals(name))
                    .findAny();
    }
    @Override
    public List<Member> findAll() {
        //store에 있는 벨류들이 멤버들인데 이게 반환이 됨
        return new ArrayList<>(store.values());
    }
}

 

다음은 구현한 코드를 검증하기 위해 테스트 코드를 작성할 것이다.

 

( 테스트를 진행할때 자바의 main기능을 통해서 실행하거나, 웹 애플리케이션의 컨트롤러를 통해 해당 기능을 실행할수있다.

하지만 이러한 방법으로 하면 실행시 오랜 시간이 걸리고 반복실행이 어려우며 테스트를 한번에 실행하기 어렵다는 단점이 있다.

따라서 JUint을 통하여 테스트를 실행할것이다. )

 

package kimtaeyoung.hellospring.repository;

import kimtaeyoung.hellospring.domain.Member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.util.List;

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

class MemoryMemberRepositoryTest {

    MemoryMemberRepository repository = new MemoryMemberRepository();

    //테스트가 하나씩 끝날때마다 저장소를 다 지움
    //why? 테스트 순서는 무작위로 돌아가기에 다시 써야되는 값들이 미리 들어가있거나 하는 에러를 방지하기위해
    //테스트가 하나씩 종료될때마다 저장소를 지워주는 메서드를 만들어야함 여기서 aftereach가 콜백함수처럼 매번 실행되는 것
    @AfterEach
    public void afterEach() {
        repository.clearStore();
    }

    @Test
    public void save() {
        Member member = new Member();
        member.setName("spring");
        repository.save(member);
        //여기서 값을 꺼낼때 이 findbyid가 옵셔넗이라 뒤에 .get()을 사용하여서 값을 꺼내줌
        //근데 이게 좋은 방법은 아님
       Member result = repository.findById(member.getId()).get();
       //System.out.println("result = " + (result == member));
        //이 어설트이퀄에서 첫번째 매개변수는 내가 기대하는것, 두번째가 실제 결과
        assertEquals(member, 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);
        //이렇게 해주면 spring 1이라는 회원과 spring2라는 회원이 가입이 된 상태

        //아까와 같이 옵셔넣은 get으로 꺼내주고 assertequal을 이용하여서 기대하는 값과 실제 결과값 비교하기
        // 그래서 member1의 이름이 spring1이니까 여기서 spring1을 찾는 걸해보면 정상 동작됨
        Member result = repository.findByName("spring1").get();
        assertEquals(member1, 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);

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

        assertEquals(2,result.size());
    }
}

 

테스트 코드는 이런식으로 작성해주었다.

테스트 코드는 한번에 실행할경우 순서를 가늠하기 어렵기에 따로 storeclear라는 메서드를 만들어주고 그걸 @AfterEach 어노테이션이 부여된 afterEach 메서드를 사용하여서 매번 테스트 후 저장소를 비워주어서 데이터로 인한 에러를 막아줘야한다.

 

기능개발후에 테스트 코드를 작성하여 테스트 하는 방법도 있지만 테스트 코드작성 후에 기능개발을 하는 방법도 있다.

즉, 테스트 코드작성은 중요하다

 

다음은 회원 서비스 클래스를 만들것이다(회원 레포와 도메인을 활용하여 실제 비즈니스 로직을 작성)

 

멤버 서비스 클래스에는 회원가입(이름중복 회원 판별),전체회원 조회, 단일회원조회 기능을 간단하게 넣었다.

public class MemberService {

    private final MemberRepository memberRepository = new MemoryMemberRepository();

    //회원가입
    public Long join(Member member) {
        //메서드로 빼줌
        validDuplicateMember(member); // 중복 회원 검증
        memberRepository.save(member);
        return member.getId();
    }

    //같은 이름 중복 회원 판별 메서드
    private void validDuplicateMember(Member member) {
        Optional<Member> result = memberRepository.findByName(member.getName());
        //ifpresent는 저 result에 널이 아니라 어떤 값이 있으면 동작하는 것 이게 옵셔넗이기에 가능한것
        //멤버를 그냥 m이라고 한것
        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);
    }
}

 

다음은 아까와 마찬가지로 이 기능들이 잘 동작되는지 테스트 코드를 작성할 것이다.

(꿀팁! 클래스 이름에 커멘드+쉬프트+t를 해주면 바로 테스트 케이스 작성 단축키)

테스트 코드를 작성할때

이런식의 given when then 문법을 사용하면 좋음

given: 무언가가 주어짐

when: 무언가를 실행했을때 

then: 이런 결과가 나와야함

(테스트가 길면 이런식으로 배 가슴 다리 식으로 나누면 보기 좋음)

ackage kimtaeyoung.hellospring.service;

import kimtaeyoung.hellospring.domain.Member;
import kimtaeyoung.hellospring.repository.MemberRepository;
import kimtaeyoung.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.*;

class MemberServiceTest {

    MemberService memberService = new MemberService();
    MemoryMemberRepository memberRepository = new MemoryMemberRepository();
    //마찬가지로 매번 테스트마다 메모리 초기회

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

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

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

        //then
        Member findMember = memberService.findOne(saveId).get();
        assertEquals(member.getName(), findMember.getName());
    }

    //테스트 코드 작성시 예외처리 부분도 중요하기에
    //아까 처리한 이름중복이되는지 테스트 코드도 작성
    @Test
    void sameNameJoin() {
        //given
        Member member1 = new Member();
        member1.setName("spring");

        Member member2 = new Member();
        member2.setName("spring");

        //when
        memberService.join(member1);
        //밑에 try catch 문법 대신에 이런 함수를 활용해서 써도됨
        //() -> memberService.join(member2) 이걸 실행했을때 IllegalStateException.class 이게 터져야한다는뜻
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
        assertEquals("이미 존재하는 회원입니다.", e.getMessage());
//        try{
//            memberService.join(member2);
//            fail();
//        } catch (IllegalStateException e) {
//            //발생되는 메세지랑 작성해뒀던 메세지랑 출력값을 비교하여 제대로 되는지 비교
//            assertEquals("이미 존재하는 회원입니다.", e.getMessage());
//        }
        //then

    }
    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

멤버 서비스의 테스트 코드를 작성해주었다.

 

스프링 빈과 의존 관계 설정

- 회원 컨트롤러가 회원 서비스와 회원 리포지토리를 사용할수있게 의존관계 준비

 

멤버 컨트롤러가 멤버 서비스를 통해 회원가입을 시켜야하고 멤버 서비스를 통해 회원 조회를 할 수 있어야함(의존 관계가 있다)

controller에노테이션이 있으면 스프링에서 이 에노테이션이있는 것을 가지고있는데 이것을 스프링 빈이 관리되고있다라고함

 

이렇게 스프링 컨테이너에 hellocontroller가 있는데 컨트롤러 에노테이션이있으면 스프링에 딱떠서 스프링에서 관리를 해줌