가랑비에 옷 젖는 줄 모른다 💻/스프링

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

잔뜩 2023. 9. 25. 22:55

컨트롤러 : 웹 MVC의 컨트롤러 역할

서비스 : 핵심 비즈니스 로직 구현

리포지토리 : 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리

도메인 : 비즈니스 도메인 객체 (엔티티..?)

 

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

 

Optional - null이 반환될 가능성이 있으면 감싸기

이 메서드는 매개 변수로 전달된 이름과 일치하는 멤버를 찾아서 Optional<Member>로 반환합니다. Optional은 값이 있을 수도 있고 없을 수도 있음을 나타내는 자바 유틸리티 클래스입니다. findByName 메서드는 이름이 일치하는 첫 번째 멤버를 찾아서 Optional<Member>로 감싸서 반환합니다. 만약 일치하는 멤버가 없다면, Optional.empty()를 반환할 것입니다.

 

store.values() 가 멤버들, 멤버들을 반환한다.

 

JUnit으로 테스트 해보기

이 코드는 스프링 프레임워크를 사용하여 간단한 회원 저장소 구현과 테스트를 제공합니다. 설명을 단계별로 나누어 보겠습니다:

### MemoryMemberRepository 클래스

1. `MemoryMemberRepository` 클래스는 `MemberRepository` 인터페이스를 구현하며, 회원 정보를 메모리에 저장하고 조회하는 기능을 제공합니다.
2. `store`는 `Map`으로서, 회원 데이터를 메모리에 저장합니다. 키는 회원의 아이디(`Long`)이고 값은 `Member` 객체입니다.
3. `sequence`는 새 회원에게 고유한 아이디를 할당하기 위해 사용되는 변수입니다.
4. `save(Member member)` 메소드는 새 회원을 저장하고 그 회원을 반환합니다. 아이디는 `sequence`를 증가시켜 할당합니다.
5. `findById(Long id)` 메소드는 주어진 아이디로 회원을 찾아 `Optional<Member>`로 감싸서 반환합니다. `Optional.ofNullable`은 값이 `null`일 수도 있는 객체를 안전하게 처리할 수 있도록 `Optional` 객체로 감싸줍니다.

### MemoryMemberRepositoryTest 클래스

1. `MemoryMemberRepositoryTest` 클래스는 `MemoryMemberRepository` 클래스의 기능을 테스트합니다.
2. `repository`는 테스트할 `MemoryMemberRepository` 인스턴스입니다.
3. `save` 메소드는 `@Test` 애너테이션으로 표시되어 JUnit이 이 메소드를 테스트 메소드로 인식하고 실행하도록 합니다.
4. `save` 테스트 메소드에서는 새 `Member` 객체를 생성하고 이름을 "spring"으로 설정한 후, `repository.save` 메소드를 호출하여 저장합니다.
5. `repository.findById` 메소드를 호출하여 저장된 회원을 조회합니다. `get()` 메소드를 호출하여 `Optional`에서 `Member` 객체를 추출합니다.
6. `Assertions.assertEquals` 메소드를 사용하여 저장된 회원과 조회된 회원이 동일한지 확인합니다. 이는 JUnit의 assertion 메소드로, 첫 번째 인자와 두 번째 인자가 같은지 비교하고 그렇지 않으면 테스트가 실패합니다.

이 코드에서 `MemoryMemberRepository` 클래스는 회원 정보를 메모리에 저장하고 조회하는 간단한 기능을 제공하며, `MemoryMemberRepositoryTest` 클래스는 해당 기능을 테스트합니다.

Assertions.assertEquals(A, B) 에서

A : expected (기댓값)

B : actual (실제 값)

Assertions.assertThat(A).isEqualTo(B) 에서

A : actual (실제 값)

B : expected (기댓값)

 

즉, 각 메소드별로 A와 B의 의미가 다르기 때문에

영한님께서 강의에 그렇게 내용을 포함하셨을거라 생각합니다.

 

=======================================================

예를 들어, 계산기 프로그램의 add(int, int) 메소드를 테스트한다고 했을 때

Assertions.assertEquals(8, Calc.add(3, 5)) 가 옳은 표현이며, 그 반대인

Assertions.assertEquals(Calc.add(3, 5), 8) 은 부적절한 표현입니다.

만약 add 메소드가 잘못 개발되어 Calc.add(3, 5) -> 5 가 반환된다고 가정해봅시다. 테스트에 실패했을 때, 발생하는 에러메시지를 본다면

전자 : 기댓값이 8이지만, 실제로 5가 나왔어.

후자 : 기댓값이 5이지만, 실제로 8이 나왔어.

와 같은 맥락으로 출력됩니다.

 

Assertions static으로 import 하기

save() 테스트

 

findByName() 테스트

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

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

        Member result = repository.findByName("winter").get();
    }}

optional<>로 감싸져 있는데 get()으로 꺼내서 Member result에 담음

다르면 다르다고 뜬다.

 

findAll() 테스트

클래스 레벨에서 돌리면 전체 메서드 테스트가 가능합니다.

 

 

주의

순서에 뭔가 종속되어서 결과값이 틀어지니까 오류가 났음. 이러면 안된다.

 

메서드가 실행되고 store를 clear 해주는 메서드 구현해주기

@AfterEach는 JUnit 5 (JUnit Jupiter)에서 제공하는 애너테이션이며, 각 테스트 메서드가 실행된 후에 실행되어야 하는 메서드를 지정하는 데 사용됩니다. 이 애너테이션은 테스트 간의 상태 유출을 방지하고, 각 테스트가 독립적으로 실행될 수 있도록 하는 데 도움이 됩니다.

 

여기서 afterEach 메서드는 @AfterEach 애너테이션으로 표시되어 있어서, 이 메서드는 각 테스트 메서드가 실행된 후에 호출됩니다. 이 메서드는 repository.clearStore() 메서드를 호출하여 repository의 상태를 초기화합니다. 이렇게 하면 각 테스트 메서드는 깨끗한 상태에서 시작할 수 있으며, 이전 테스트에서 변경된 상태에 영향을 받지 않게 됩니다.

오류가 해결 됐다.

 

회원서비스구현

    public Long join(Member member){
        // 같은 이름이 있는 중복회원은 안된다.
        Optional<Member> result = memberRepository.findByName(member.getName());
        result.ifPresent(m->{
            throw new IllegalStateException("이미 존재하는 회원입니다");
        });
        memberRepository.save(member);
        return member.getId();
    }
}

member 객체를 optional로 한번 더 감싸줌 . 그래서 ifPresent와 같은 메서드를 사용할 수 있다.

Optional<Member>를 반환합니다. Optional은 값이 있을 수도 있고 없을 수도 있는 컨테이너입니다.

코드가 너무 길어진 것 같다면 다음과 같이 쓸 수있다.

public Long join(Member member){
        // 같은 이름이 있는 중복회원은 안된다.
        memberRepository.findByName(member.getName())
        .ifPresent(m->{
            throw new IllegalStateException("이미 존재하는 회원입니다");
        });
        memberRepository.save(member);
        return member.getId();
    }

근데 하나의 메서드로 빼는게 좋을 것 같음 CTRL + T

package hello.hellospring.service;

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

import java.util.Optional;

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

    public Long join(Member member){
        // 같은 이름이 있는 중복회원은 안된다.
        validateDuplicateMember(member);

        memberRepository.save(member);
        return member.getId();
    }

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

서비스테스트

get()으로 optional의 Member 객체를 꺼냈기 때문에 Member findMember 가 가능한거다! 까먹지 말 것 

 

package hello.hellospring.service;

import hello.hellospring.domain.Member;

import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

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

class MemberServiceTest {
    //테스트는 한글메서드로 바꿔도 좋다.
    MemberService service = new MemberService();
    MemoryMemberRepository repository = new MemoryMemberRepository();

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

    @Test
    void 회원가입() {

        //given
        Member member = new Member();
        member.setName("spring");

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

        //then
        Member findeMember = service.findOne(saveId).get();
        Assertions.assertThat(member.getName()).isEqualTo(findeMember.getName());
    }

    @Test
    void 중복_회원_예외() {

        //given
        Member member1= new Member();
        member1.setName("spring");

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

        //when , 예외가 터져야함! 터져야 성공
        service.join(member1);
        //assertThrows(IllegalStateException.class,()->service.join(member2));
        IllegalStateException e = assertThrows(IllegalStateException.class,()->service.join(member2));
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다");


        //트라이 앤 캐치가 ...여기서 쓰기엔 적합하지 않다
//        try{
//            service.join(member2); //validate에서 걸려서 예외가 터져야 한다.
//            fail();
//        }catch (IllegalStateException e){
//            //성공
//            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
//        }

        //then
    }

    @Test
    void 회원조회() {
    }
}

근데 좀 문제점이 서비스 클래스의 리포지토리랑 테스트의 리포지토리가 다른 인스턴스여버린다.

DB가 다른것과도 마찬가지 ..그래서 서비스의 리퍼지토리를 좀 손 볼 필요가 있다.

서비스에 생성자처럼 넣어주기!!

 

 

왜 바꾸는건지 이해가 안됐는데 321이 설명해줌 ㅠ

 

dependency injection DI와 같다!

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;
    }

    public Long join(Member member){
        // 같은 이름이 있는 중복회원은 안된다.
        validateDuplicateMember(member);

        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 memberId){
        return memberRepository.findById(memberId);
    }
}
package hello.hellospring.service;

import hello.hellospring.domain.Member;

import hello.hellospring.repository.MemberRepository;
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 static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertThrows;

class MemberServiceTest {
    //테스트는 한글메서드로 바꿔도 좋다.
    MemberService service;
    MemoryMemberRepository repository;
    @BeforeEach
    public void beforeEach(){
        repository = new MemoryMemberRepository();
        service = new MemberService(repository);
    }

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

    @Test
    void 회원가입() {

        //given
        Member member = new Member();
        member.setName("spring");

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

        //then
        Member findeMember = service.findOne(saveId).get();
        Assertions.assertThat(member.getName()).isEqualTo(findeMember.getName());
    }

    @Test
    void 중복_회원_예외() {

        //given
        Member member1= new Member();
        member1.setName("spring");

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

        //when , 예외가 터져야함! 터져야 성공
        service.join(member1);
        //assertThrows(IllegalStateException.class,()->service.join(member2));
        IllegalStateException e = assertThrows(IllegalStateException.class,()->service.join(member2));
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다");


        //트라이 앤 캐치가 ...여기서 쓰기엔 적합하지 않다
//        try{
//            service.join(member2); //validate에서 걸려서 예외가 터져야 한다.
//            fail();
//        }catch (IllegalStateException e){
//            //성공
//            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
//        }

        //then
    }

    @Test
    void 회원조회() {
    }
}

 

728x90