인프런/스프링 입문

5)회원 서비스 만들기(회원가입..등),테스트하기,static변수특징,생성자사용하기,인터페이스객체,의존성주입

backend dev 2022. 10. 29.

회원서비스 만들기

service 패키지 생성후 MemberService.java를 생성

 

회원가입 메소드 만들기

MemberService.java에 회원가입 메소드를 만드려고 한다.

그전에 이름을 가지고 이미 같은이름을 가진 멤버가 있는지 체크하려고 한다 (중복검사)

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

이런식으로 findbyname의 리턴값이 optinal이니까 변수에 저장해놓고 ifPresent 함수를 실행하여 진행 할 수 있다.

ifPresent()는 optinal에 사용되며 안에는 람다식이나 메소드 레퍼런스가 올수 있다 여기서는 Consumer(함수형 인터페이스)를 사용했다. Consumer도 람다로 구현가능하다.

m은 매개변수 느낌인데 사용해도 그만 안해도 그만인 느낌인거같다.

 

Java 8 - Consumer 예제

Consumer는 1개의 Type T 인자를 받고 리턴 값이 없는 함수형 인터페이스입니다. Consumer는 Lambda 표현식으로 구현할 수 있습니다. accept() 호출과 함께 인자를 전달하면 구현된 내용이 수행됩니다. andThe

codechacha.com

 

자바8 Optional 3부: Optional을 Optional답게

Engineering Blog by Dale Seo

www.daleseo.com

 

위와같이 코드를 짰을때 리팩토링

-> 어차피 findbyname의 리턴은 optinal이고 optional에다가 ifPresent 메소드를 사용할것이니까 다음과 같이 줄일 수 있다.

 

memberRepository.findByName(member.getName()).ifPresent(m -> { // findbyname이라는 메소드의 리턴값은 optinal임 거기다가 바로 ifpresent를 사용한것임
    throw new IllegalStateException("이미 존재하는 회원입니다");
});

findbyname의 리턴값을 받고 바로 ifPresent 하는 느낌으로 코드를 더 깔끔하게 짤 수 있음

 

 

추가적인 리팩토링

public Long join(Member member) {


    //같은 이름이 있는 회원은 안됨 (중복방지-> 임의로 지정한 규칙)
    //findbyname은 optinal<member>를 리턴한다.
    //.get()을 이용해서 직접 꺼낼수도 있지만 권장하지않는다 -> optinal을 이용하는 이유가 null일수도 있어서 사용한거라
    //아니면 값이있으면 꺼내고 아니면 안에있는 메소드를 실행하는 .orElseGet 이라는 메소드가 있다.
    //따로 optinal 변수에 담고 .findbyname을 실행할 수 있지만 더 깔끔하게 -> 리팩토링
    memberRepository.findByName(member.getName()).ifPresent(m -> { // findbyname이라는 메소드의 리턴값은 optinal임 거기다가 바로 ifpresent를 사용한것임
        throw new IllegalStateException("이미 존재하는 회원입니다");
    }); // .ifPresent() 만약 값이 있다면 -> 안의 람다식이나 메소드 레퍼런스가 올 수 있다. 여기서는 Consumer(함수형 인터페이스 = 람다로 구현가능)가 왔다.
    // m -> 에서 m 을 이용해도 되는데 여기서는 throw 만 해주면 되니까 throw 해줌.  Exception를 날리면서 메시지도 날려줬다.

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

}

현재 까지 진행상황이다.

 

위의 코드를 봤을떄 findbyname을 사용하는 그 부분은 이미 존재하는 회원을 검사하는 또 하나의 로직으로서 따로 빼서 메소드를 구성하는게 더 깔끔해 보인다.(+ 더 읽기쉬움)  (현재까지는 join메소드 안에서 구현중이였음)

원하는 부분을 가지고 메소드로 추출하는 방법이 있다. Extract Method인데 

Shfit + Ctrl + ALT +  T 를 누르면 리팩토링과 관련된 메소드들이 나온다.  거기에 있다.

메소드 추출 리팩토링 단축키는 ->Ctrl+Alt+M

 

Ctrl+Alt+M를 눌렀을때 위의 블럭에서 메소드이름을 지정해주면된다.  깔끔해져서 join함수 내부의 동작을 쉽게 파악할 수 있음 ( 함수의 내부동작을 읽을 필요없이 함수의 이름만으로 어떤 동작을 하는지 유추할 수 있으니까)

 

 

추가적인 Service 메소드 추가

Service의 들어가는 메소드들은 비지니스적으로 동작하므로 이름도 그렇게 지어야한다.

repository는 데이터를 넣고 빼고 , 가져오고 정도의 기계적인 동작이니 이름도 그렇게 짓는다.

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

/*
멤버 아이디를 이용한 멤버조회
 */
public Optional<Member> findOne(Long memberId)
{
    return memberRepository.findById(memberId);
}

 

service에 추가된 메소드들

 

전체 Service코드

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) {
        memberRepository.findByName(member.getName()).ifPresent(m -> { // findbyname이라는 메소드의 리턴값은 optinal임 거기다가 바로 ifpresent를 사용한것임
            throw new IllegalStateException("이미 존재하는 회원입니다");
            // .ifPresent() 만약 값이 있다면 -> 안의 람다식이나 메소드 레퍼런스가 올 수 있다. 여기서는 Consumer(함수형 인터페이스 = 람다로 구현가능)가 왔다.
            // m -> 에서 m 을 이용해도 되는데 여기서는 throw 만 해주면 되니까 throw 해줌.  Exception를 날리면서 메시지도 날려줬다.
            //findbyname은 optinal<member>를 리턴한다.
            //.get()을 이용해서 직접 꺼낼수도 있지만 권장하지않는다 -> optinal을 이용하는 이유가 null일수도 있어서 사용한거라
            //아니면 값이있으면 꺼내고 아니면 안에있는 메소드를 실행하는 .orElseGet 이라는 메소드가 있다.
            //따로 optinal 변수에 담고 .findbyname을 실행할 수 있지만 더 깔끔하게 -> 리팩토링
        });
    }
    /*
    전체 회원 조회
     */
    public List<Member> findMembers() {
        return memberRepository.findAll();
    }

    /*
    멤버 아이디를 이용한 멤버조회
     */
    public Optional<Member> findOne(Long memberId)
    {
        return memberRepository.findById(memberId);
    }





}

 

만들어놓은 코드에 대한 검증방법

1. 메인메소드를 만들어 실험하는 방식

2. 컨트롤러를 이용해서 직접 디비에 넣고 테스트하는 방식

3. 제일좋은 방법 -> 이전 게시글에서 배운 테스트케이스를 활용하는 방법이다!

 

 

MemberService 테스트 해보기 

테스트를 위해 자바파일 만들고 막 귀찮게 안해도 되게끔

테스트파일을 자동생성해주는 명령어

클래스 내에서 다음 단축키를 실행

Ctrl  +Shfit +  T 

 

이전 게시글에는 Test디렉토리 가서 디렉토리 만들고 파일 만들고 했음

 

 

다음과 같은 화면이 나오는데 

JUnit5 기본선택되어있는거 냅두고

 

밑에 테스트를 원하는 메소드를 선택한다!

테스트안에 service라는 디렉토리가 자동으로 생기고 그안에 테스트파일이 생성되었다!

이런식으로 생성되어있음!

Assertions도 이미 static import가 되어있다! ( 하지만 이 assertion이 아님 , assertj꺼를 쓴다)

이거

Assertions 할때 assertj 꺼를 선택해주고 ,

Assertions 계속 쓰는게 불편하니까 

Assertions.assertThat(member.getName()).isEqualTo(findMember.getName());

Assertions 를 선택하고 

Alt + enter 를 해주고 

 

add on-deman static import for~ 를 선택해준다. 

그러면 

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

이게 생기면서 이제 Assertions를 안쳐도됨! 

assertThat(member.getName()).isEqualTo(findMember.getName());

깔끔하게 바뀐다.

 

테스트 메소드는 한글로도 이름을 정할 수 있음

메소드명을 한글로 해두면 나중에 결과창으로 어떤 메소드가 성공한지 파악하기 쉬우니까 

영어권 회사가 아니면 한글로 많이 해둔다고 한다.

 

 

given , when , then

given -> 어떤값이 주어졌을때

 

when -> 실행했을때

 

then -> 결과 체크

 

이렇게 3개로 구분하여 테스트메소드를 작성한다고 한다.

회원가입 테스트 메소드 모습

    @Test
    void 회원가입() { //테스트 메소드는 한글로도 이름을 정할 수 있음.
        //given 값이 주어졌을때
        Member member = new Member();
        member.setName("hello");

        //when  실행했을때
        Long saveId = memberService.join(member);

        //then 결과가 이렇게 나와야한다.
        Member findMember = memberService.findOne(saveId).get(); // .get을 이용해서 optional을 벗긴 멤버값을 받을 수 있다.
        assertThat(member.getName()).isEqualTo(findMember.getName());//방금만든 멤버의 이름이 나와야한다.
        //그러므로 멤버의 이름이 실제값, findmember 했을때 나오는 이름이 예상값

    }

assertThat().isEqualTo -> 실제값 , 예상값 순으로 넣는다.   예상값-> expected (이 값이 나와야 성공)

실패하면 이런식으로 나옴.

 

 

테스트는 정상로직도 중요하지만 오류발생 체크도 중요하다.

 

중복_회원_예외()

try_catch를 넣는 방법 -> 깔끔하지못함.

 public void 중복_회원_예외() {
        //given
        Member member1 = new Member();
        member1.setName("spring");

        Member member2 = new Member();
        member2.setName("spring");
        //when
        memberService.join(member1); // member1을 회원가입
        try {
            memberService.join(member2); // member2를 회원가입 시킬때 중복된 이름으로 인해 오류가 발생해야함
            //이 다음줄로 넘어간다는것은 오류가 발생해야하는데 catch로 가지않았다는것이므로
            fail();// 테스트 실패!

        }catch (IllegalStateException e )
        {
            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다"); //catch에 아무것도 안적어도 try catch로 잘넘어갔기때문에 테스트하면 성공이 뜬다.
        }

        //then
    }

catch안에 아무 코드를 넣지않아도 , catch로 잘넘어갔다면 실패가 뜨지않는다.

하지만 오류가 발생하는 코드

memberService.join(member2); 

를 그냥 지나가고(catch로 안가고) 다음으로 넘어가면 실패하게끔 하기위해 fail()를 넣어논다.

 

많이쓰는방법 (깔끔)

public void 중복_회원_예외() {
    //given
    Member member1 = new Member();
    member1.setName("spring");

    Member member2 = new Member();
    member2.setName("spring");
    //when
    memberService.join(member1); // member1을 회원가입
    assertThrows(IllegalStateException.class, () -> memberService.join(member2)); //발생해야하는 예외.class , 발생시키는 메소드(람다형식)으로 입력
    }

assertThrows를 사용한다.

https://covenant.tistory.com/256

 

완벽정리! Junit5로 예외 테스트하는 방법

환경 구성 testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeClasspath - Runtime classpath of source set 'test'. +--- org.springframework.boot:spring-boot-starter-web -> 2.5.6 \--- org.springframework.boot:spring-boot-sta

covenant.tistory.com

assertThrows(IllegalStateException.class, () -> memberService.join(member2)); //발생해야하는 예외.class , 발생시키는 메소드(람다형식)으로 입력

발생해야하는 예외.class , 발생시키는 메소드(람다형식)으로 입력한다.

해당 메소드가 실제로 지정한 예외를 발생시키면 성공!

아니면 실패!

 

 

예외 메시지를 검증하고싶다면

IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));// 메시지검증을 하고싶다면 리턴값을 저장해서 
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다"); //검증한다.

assertThrows의 리턴값(예외)를 받아 담아서 메시지를 검증하는 방법

 

 

테스트메소드마다 실험하는 값들이 중복될 수 있다. 중복될때 오류를 막기위해

MemoryMemberRepository memberRepository = new MemoryMemberRepository(); // 저장소 내용 삭제를 하기위해  저장소 객체생성
//저장소객체안에서 실제 데이터를 저장하는 map 변수는 static이므로 다른객체를 만들어도 공용으로 사용하고 ,어느 객체에서든 접근가능하다.

@AfterEach //하나의 테스트 메소드가 동작이 끝날때마다 해당 어노테이션이 붙은 메소드를 동작하라는뜻.
public void afterEach()
{
    memberRepository.clearStore(); // 저장소에있는 내용 삭제
    //저장소역할을 하는 store 변수와 멤버아이디를 만들기위한 sequence 변수는 static 이므로
    //위에처럼 새롭게 저장소객체를 만들어도 static 변수는 공용이기떄문에 접근가능하다.
}

저장소객체를 선언해주고 , 

@AfterEach를 이용해 테스트 메소드가 실행될때마다 저장소에 저장된 임시데이터를 지우도록한다.

 

저장소 객체에는 클래스변수중 저장을 담당하는 

private static Map<Long,Member> store = new HashMap<>();

이런 변수가 있는데 static으로 선언되어 , 공용이다.

static은 다음과 같다고 한다.

static을 사용하는 또 한가지 이유로 공유 개념을 들 수 있다. static 으로 설정하면 같은 곳의 메모리 주소만을 바라보기 때문에 static 변수의 값을 공유하게 되는 것이다.

이래서 저장소객체를 만들면 어느 객체든 같은 저장소변수에 접근가능한것이다.

 

 

---

하지만 

private Map<Long,Member> store = new HashMap<>();

이것처럼 static이 아니라면 

MemoryMemberRepository memberRepository = new MemoryMemberRepository();

MemberServiceTest.java에서 새롭게 저장소 객체를 만드는것은 새로운 저장소를 만드는것과 다를게 없다!

 

static이여서 다행이지만, 혹시라도 있을 사고를 위해 수정해준다!

 

지금 상황은 MemberService.java에 있는 memberRepository 객체를 쓰지못하니까

MemberServiceTest에서 새로 memberRepository 객체를 만들어 static 변수인 store 접근 하게끔 한것이다.

 

어떻게 수정해야 MemberService.java에 있는 memberRepository 객체를 사용할 수 있을까?

현재 이런식으로 MemberService 클래스안에서 다음과같이 클래스변수를 선언 및 초기화까지 하고있다.

 

이렇게 수정해준다.

public class MemberService {


    private final MemberRepository memberRepository ;//여기서는 멤버저장소 인터페이스를 선언한다. 그렇게 함으로서 해당 인터페이스를 구현한 저장소객체를 다 받을수 있기떄문.

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

Alt + Insert로 생성자를 쉽게 만들 수 있다.

 

이렇게 하면 저장소는 선언까지만 하고

초기화는 생성자를 이용해서  MemberService를 생성할때 저장소를 같이 넘겨서 외부에서 저장소를 받아서

MemberService안의 저장소를 초기화 시켜준다!

 

멤버저장소 인터페이스 객체를 선언 한 이유 :  해당 인터페이스를 구현한 저장소객체를 다 받을수 있기 때문

 

 

수정된 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 // beforeEach는 각 테스트메소드가 실행되기전에 실행되는 메소드를 설정하기위한 어노테이션
    public void beforeEach() {
        memberRepository = new MemoryMemberRepository(); // 새로 저장소를 생성해서 초기화해주고
        memberService = new MemberService(memberRepository); // 새로 생성한 저장소를 전달해서 멤버서비스까지 생성 및 초기화 해준다.
        //이렇게 함으로서 모든 테스트케이스가 시작전에 새로운 멤버저장소,멤버서비스를 가지기 떄문에 다른 테스트 메소드의 영향을 받을일이 없어진다.
        //테스트 메소드는 독립적으로 실행이 되야하기 때문에 다음과 같은 설정을 한다.
    }

    @AfterEach //하나의 테스트 메소드가 동작이 끝날때마다 해당 어노테이션이 붙은 메소드를 동작하라는뜻.
    public void afterEach()
    {
        memberRepository.clearStore(); // 저장소에있는 내용 삭제
        //저장소역할을 하는 store 변수와 멤버아이디를 만들기위한 sequence 변수는 static 이므로
        //위에처럼 새롭게 저장소객체를 만들어도 static 변수는 공용이기떄문에 접근가능하다.
    }


    @Test
    void 회원가입() { //테스트 메소드는 한글로도 이름을 정할 수 있음.
        //given 값이 주어졌을때
        Member member = new Member();
        member.setName("spring");

        //when  실행했을때
        Long saveId = memberService.join(member);

        //then 결과가 이렇게 나와야한다.
        Member findMember = memberService.findOne(saveId).get(); // .get을 이용해서 optional을 벗긴 멤버값을 받을 수 있다.
        assertThat(member.getName()).isEqualTo(findMember.getName()); //방금만든 멤버의 이름이 나와야한다.
        //그러므로 멤버의 이름이 실제값(actual) findmember 했을때 나오는 이름이 예상값(expected)

    }

    @Test
    public void 중복_회원_예외() {
        //given
        Member member1 = new Member();
        member1.setName("spring");

        Member member2 = new Member();
        member2.setName("spring");
        //when
        memberService.join(member1); // member1을 회원가입
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));// 메시지검증을 하고싶다면 리턴값을 저장해서
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다"); //검증한다.

//        try {
//            memberService.join(member2); // member2를 회원가입 시킬때 중복된 이름으로 인해 오류가 발생해야함
//            //이 다음줄로 넘어간다는것은 오류가 발생해야하는데 catch로 가지않았다는것이므로
//            fail();// 테스트 실패!
//
//        }catch (IllegalStateException e )
//        {
//            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다"); //catch에 아무것도 안적어도 try catch로 잘넘어갔기때문에 테스트하면 성공이 뜬다.
//        }

        //then
    }


    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

 

MemberService memberService; //멤버 서비스 선언
MemoryMemberRepository memberRepository;//멤버 저장소 선언

@BeforeEach // beforeEach는 각 테스트메소드가 실행되기전에 실행되는 메소드를 설정하기위한 어노테이션
public void beforeEach() {
    memberRepository = new MemoryMemberRepository(); // 새로 저장소를 생성해서 초기화해주고
    memberService = new MemberService(memberRepository); // 새로 생성한 저장소를 전달해서 멤버서비스까지 생성 및 초기화 해준다.
    //이렇게 함으로서 모든 테스트케이스가 시작전에 새로운 멤버저장소,멤버서비스를 가지기 떄문에 다른 테스트 메소드의 영향을 받을일이 없어진다.
    //테스트 메소드는 독립적으로 실행이 되야하기 때문에 다음과 같은 설정을 한다.
}

 

@BeforeEach를 통해서 각 테스트 메소드가 실행되기전에 

멤버저장소 객체를 생성해 초기화하고 

멤버서비스 객체를 생성할때 전달해줌으로서  각 테스트 메소드가 실행될때 각 메소드별로 저장소를 따로 가지게 되고 

테스트 메소드가 독립적으로 실행되게끔한다 ( 다른 테스트 메소드에게 영향을 받지않고)

 

MemberService 입장에서는 

public class MemberService {


    private final MemberRepository memberRepository ; //여기서는 멤버저장소 인터페이스를 선언한다. 그렇게 함으로서 해당 인터페이스를 구현한 저장소객체를 다 받을수 있기떄문.

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

자신이 직접 저장소를 생성해서 초기화하지않고 외부에서 생성에서 넘겨준다.

이런것을 

Dependency injection (의존성 주입)  == DI

이라고 한다 

 

의존성주입에 대한 자세한 내용은 다음시간에

댓글