스프링 입문 - Ch 3. 회원 관리 예제 - 백엔드 개발(3)

seren-dev·2022년 3월 22일
0

스프링 입문

목록 보기
5/11

회원 서비스 개발

회원 서비스회원 레포지토리와 도메인을 활용해서 실제 비즈니스 로직을 작성

  • src/main/java/hello.hellospring 패키지에서 service 패키지 생성
  • MemberService 클래스 생성

  • 리포지토리 생성
  • 서비스 클래스비즈니스 용어를 갖고 써야 한다.
    ex) join, findMembers
  • 서비스는 비즈니스 의존적으로 설계
  • 리포지토리는 단순히 기계적, 개발에 쓰이는 용어

final 키워드

final 키워드가 붙으면 주입이 되었을 때 다른 값으로의 변경이 불가능한 불변객체가 됩니다.
아래의 링크에서 생성자 주입(final 키워드에 관한) 을 참고해주세요.
https://yaboong.github.io/spring/2019/08/29/why-field-injection-is-bad/
private final~ 에서 final을 빼도 생성자 주입은 됩니다. 다만 final 키워드가 붙은 객체에 대한 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없고 불변하게 설계할 수 있습니다.
https://www.inflearn.com/questions/234075
final로 생성한 이유는 실행도중에 동적으로 MemoryMemberRepository를 다른 객체로 바꾸지 말라는 의미입니다. 현재 의도가 처음 생성시점에 객체를 확정하고, 이후에 변경하면 안되다는 것을 명시적으로 지정한 것이지요. final을 붙이면 만약 실행 도중에 다른 객체로 바꾸는 코드가 나타나면 컴파일 오류가 발생해서, 중간에 바뀌는 문제를 예방할 수 있습니다.

Repository와 Service는 역할이 각각 다릅니다.
Repository는 DB와 밀접한 객체입니다. DB에 접근하여 데이터를 조회, 저장, 삭제 등의 역할을 가집니다.
Service는 핵심 비즈니스 로직을 가지는 객체입니다.
예를 들어, 각각의 역할을 분리해놓음으로써 문제가 발생할 시 비즈니스 로직 상 문제가 없는것 같은데 DB 관련 처리에서 문제가 발생한다면 Repository코드를 확인해보면 됩니다.
각각의 역할이 분리되어 개발과 유지보수 등이 용이해집니다.

ifPresent

이 메소드는 특정 결과를 반환하는 대신에 Optional 객체가 감싸고 있는 값이 존재할 경우에만 실행될 로직을 함수형 인자로 넘길 수 있습니다.
자바에서 람다 본문표현식이거나 블록이여야 하는데 thorw는 명령문이라서 블록을 지우면 사용할 수 없다고 하네요.
https://www.daleseo.com/java8-optional-effective/

전체 코드

MemberService.java

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) {

        validateDuplicateMember(member);    //중복 회원 검증

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

    private void validateDuplicateMember(Member member) {
        //Optional<Member> result = memberRepository.findByName(member.getName());
        memberRepository.findByName(member.getName())
                .ifPresent(m -> {
                        //null일 가능성이 있으면 Optional로 감싸서 반환, 값이 있으면 Optional의 ifPresent() 메소드 사용
                        // result.get()으로 멤버 얻기
                        throw new IllegalStateException("이미 존재하는 회원입니다.");
                });
    }

    /**
     * 전체 회원 조회
     */
    public List<Member> findMembers() {
        return memberRepository.findAll();
    }

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

회원 서비스 테스트

  • 테스트 메소드는 한글로 적어도 됨
  • 테스트 라이브러리는 빌드될 때 포함 안됨

테스트 코드 작성법

모든 메소드마다

    //given 뭔가가 주어졌는데

    //when 이걸 실행했을 때

    //then 결과가 이렇게 나와야 돼
    

예외 테스트 코드 작성법

  1. try-catch 문
	memberService.join(member1);
	try {
            memberService.join(member2);
            fail();
        } catch (IllegalStateException e) {
            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.23132");
        }

예외가 발생한 경우

org.opentest4j.AssertionFailedError: 
expected: "이미 존재하는 회원입니다.23132"
 but was: "이미 존재하는 회원입니다."
Expected :"이미 존재하는 회원입니다.23132"
Actual   :"이미 존재하는 회원입니다."
  1. assertThrows() 사용
    import static org.junit.jupiter.api.Assertions.assertThrows;
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
//이 로직을 넣으면 이 예외가 터져야 함

assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

전체 클래스 테스트

MemberService memberService = new MemberService();
MemoryMemberRepository memberRepository = new MemoryMemberRepository();

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

오류없이 잘 돌아가는 이유?

  • MemoryMemberRepository의 store 변수는 static이기 때문에
  • static은 인스턴스와 상관없이 클래스 레벨에서 접근함

But
MemberService의 memberRepository와
테스트의 memberRepository는 다른 객체

해결책
MemberService.java

public class MemberService {

    private final MemberRepository memberRepository;

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

MemberServiceTest.java

    MemberService memberService;
    MemoryMemberRepository memberRepository;

    @BeforeEach
    public void beforeEach() {
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository);
        //DI
    }
  • @BeforeEach는 매 테스트 실행 전 호출된다. 테스트가 서로 영향이 없도록 항상 새로운 객체를 생성하고, 의존관계도 새로 맺어준다.
    호출 전 동일한 환경을 세팅하기 위해 사용된다.

MemberService.java파일에서 public MemberService(MemberRepository memberRepository)
구체적인 타입에 의존하지않고 추상적인 타입에 의존하게 됨으로써 의존성주입을 통한 유연한 코드 사용이 가능하도록 설계한 것입니다.

검증되지 않은 메서드를 사용한 테스트 통과는 테스트 신뢰성에 대한 문제가 생깁니다.
하지만 간단한 메서드에 대해서는 효율적인 측면에서 테스트 작성을 건너띄는것은 선택일 것 같네요.
핵심 비즈니스와 관련된 메서드 혹은 그와 관련된 메서드라면 테스트로 확실한 검증을 하는 것이 좋습니다.

JUnit은 각 테스트 메서드를 실행할 때 마다 MemberServiceTest 객체 자체를 새로 만들어서 테스트 하기 때문에, 기존 코드도 다음 테스트를 실행할 때면 이미 memberService, memoryMemberRepository 객체가 가비지 되고, 새로 만들어집니다.
따라서 BeforeEach를 사용해도 되고 안 사용해도 됩니다. 둘 중 아무것이나 사용해도 됩니다. 단순하면 필드에서 직접 처리해도 되고, 초기화가 복잡해지면 @BeforeEach를 사용하면 됩니다.

전체 코드

MemberServiceTest.java

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.Optional;

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

class MemberServiceTest {

    MemberService memberService;
    MemoryMemberRepository memberRepository;

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

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

    @Test
    void 회원가입() {
        //given
        Member member = new Member();
        member.setName("spring");

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

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

    @Test
    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));
        //이 로직을 넣으면 이 예외가 터져야 함

        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

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

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

0개의 댓글