Spring

스프링 기초2

oyatplum 2024. 1. 27. 18:42

 

회원 관리 예제를 만들어 봅시다.

 

  • 비즈니스 요구 사항 정리
  • 회원 도메인과 레포지토리 생성
  • 회원 레포지토리 테스트 케이스 작성
  • 회원 서비스 개발
  • 회원 서비스 테스트

 

1.  비즈니스  요구 사항 정리

가장 간단한 요구 사항으로 진행합니다.

 

- 데이터 : 회원id, 이름

- 기능 : 회원 등록, 조회

- 아직 데이터 저장소가 선정되지 않음(가상의 시나리오. 즉 db가 결정되지 않음)

 

 

컨트롤러는 지금까지 사용했던 컨트롤러.

서비스는 '중복 회원 가입 불가' 등의 핵심 비즈니스 로직.

도메인에는 데이터 베이스에서 저장하고 관리하는 객체

리포지토리는 도메인 객체를 가지고 핵심 비즈니스 로직(서비스)이 동작하도록 구현.

 

 

구현할 클래스의 의존 관계는 다음과 같습니다.

 

회원 비즈니스 로직에는 MemberService.

회원을 저장하는 리포지토리는 MemberRepository로 interface로 설계를 할 것입니다.

아직 데이터 저장소가 선정되지 않았기 때문입니다.

그리고 구현체는 MemoryMemberRepository로 만들 것 입니다. (저장소가 없어도 개발은 우선 할 것이니까.. memory로)

 


2.  회원 도메인과 레포지토리 생성

 

1.

회원 객체를 만들기 위해 domain package에 Member class를 생성했습니다.

 

id는 고객이 저장하는 것이 아니라 시스템이 저장하는 아이디입니다. (데이터 구분을 위함)

 


 

2.

그리고 회원 객체의 저장소를 만들기 위해 repository package에 MemberRepository interface를 생성했습니다.

 

save는 회원을 저장소에 저장합니다.

findById와 findByName은 말 그대로 id와 name으로 회원을 찾습니다.

여기서 Optional은 우선 간단히 말해 반환된 값이 null일 수 있기 때문에 null을 그대로 반환하지 않고 Optional로 감싸서 반환하도록.. 해주는 기능을 합니다! (null이면 Optional.empty()로 반환됨)

findAll은 저장된 모든 회원 리스트를 반환합니다.

 


 

3.

이제 구현체를 만들기 위해 MemoryMemberRepository를 생성했습니다.

 

방금 생성한 MemberRepository를 implements 해줍니다. (option + enter로 모든 메소드 implements)

(그러면 자동으로 MemberRepository에 있는 함수들이 @Override 어노테이션과 함께 오버라이딩됩니다.)

(overriding : 부모 클래스에서 정의된 메소드를 자식 클래스에서 재정의)

 

memory에 save하기 위해 어딘가에 저장을 해야겠죠? Map을 사용했습니다.

Map은 key, value 형태로 데이터를 저장할 수 있는 자료구조로 Long 타입의 id와 Member 객체를 값으로 가지도록 생성했습니다. 이때 HashMap을 사용했는데 이는 Map 인터페이스의 구현체 중 하나입니다.

이렇게 회원 저장소 store를 생성했습니다.

 

sequence는 회원 객체에 할당되는 고유한 키 값을 생성하기 위한 변수입니다.

 

 

 

 

우선 회원을 save하기 위해 setId할 때 sequence를 하나 올려주고

store.put(id, member 객체)메서드를 통해 저장한 뒤 member 객체를 반환합니다.

 

findById는 store.get(id)로 id를 가져올 수 있지만 null인 경우를 감안하며 Option.ofNullable()로 감싸줍니다.

 

 

findByName의 경우에는 위와 같습니다.

우선 store.values()는 맵에 저장된 모든 값(collection 형태로)을 반환합니다. 이때 stream()을 사용해 반환된 값(stream 형태로)을 돌리면서 filter()를 통해 member의 name과 인자의 name이 같은 지 필터링하고 findAny()를 통해 찾은 값을 반환을 합니다.

 

findAll은 반환 값이 List이기 때문에 map 형태인 store 앞에 ArrayList 생성자를 통해 맵에 있는 모든 값을 List 형태로 반환합니다.

 


3.  회원 레포지토리 테스트 케이스 작성

 

테스트는 JUnit(java용 테스트 프레임워크)와 AssertJ 라이브러리를 사용했습니다.

 

위에서 작성한 MemoryMemberRepository를 테스트하기 위해

repository package 내부에 MemoryMemberRepositoryTest를 생성했습니다.

 

 


1.

우선 save 메소드의 test를 작성해봅시다.

 

우선 MemoryMemberRepository 클래스의 객체인 repository를 생성했습니다.

Member 클래스로 member 객체를 생성한 뒤 이름은 spring으로 지정하고 save 했습니다.

 

이 과정이 제대로 save되었는지 확인하기 위해

repository.findById(member.getId())를 통해 회원의 id를 가져옵니다. 이때 반환 타입은 Optional이기 때문에 get()메소드를 사용하여 회원 객체를 가져올 수 있습니다. (command + option + v : Member result = 처럼 변수 자동 추출)

 

제대로 가져왔는지 확인하기 위해 주석 처리한 부분처럼 직접 확인해볼 수 있지만 계속 글씨로 확인할 수는 없으니까~

Assertions 기능을 활용합니다.

 

JUnit이 제공하는 assertEquals기능도 있지만 AssetJ에서 제공하는 assertThat을 사용했습니다. (더 많이 쓰인다고 함)

Assertions.assertThat(member).isEqualTo(result);를 통해 member 객체와 result 객체를 비교합니다.

이때 Assertions는 import로 뺄 수 있습니다.(option + enter)

 


2.

findByName과 findAll 메소드를 테스트 해봅시다.

 

거의 유사합니다.

 

한 가지 추가적으로 설명하자면.. findByName에서

get()을 사용하지 않으면 Optional<Member> result = repository.findByName("");으로 써야 합니다.

get()을 쓰면 optional을 벗겨준다는..? 느낌이죠.

 


 

모든 테스트 케이스를 돌려보면

 

에러가 납니다.

당연합니다.

 

테스트 순서는 보장이 안 됩니다.

모든 테스트는 순서와 상관없이 개별적으로 동작하도록 설계해야 합니다.

 

위와 같은 경우 findAll 메소드에서 이미 데이터가 저장되었기 때문에 findByName 메소드에서 에러가 난 것입니다.

 

그러면 어떻게 해야할까요?

 

 

 

 

 

테스트가 끝날 때마다 데이터를 초기화시키면 됩니다.

 

 

MemoryMemberRepository로 돌아가 clearStore 메소드를 생성하고 store.clear로 데이터를 삭제해줍니다.

 

 

그런 뒤 테스트 케이스에 afterEach 메소드를 생성하여 clearStore를 통해 데이터를 삭제해줍니다.

이때 @AfterEach 어노테이션을 써줘야 하는데 이는 하나의 메소드가 끝날 때마다 실행되는 콜백 메소드입니다.

 

 

이렇게 하면

 

모든 test가 깔끔하게 통과하는 것을 확인할 수 있습니다.


4.  회원 서비스 개발

 

회원 서비스 클래스를 작성해 보겠습니다.

회원 서비스는 위에서 작성한 레포지토리와 도메인을 활용해서 실제 비즈니스 로직을 구현하는 부분입니다.

 

 

service package에 MemberService 클래스를 생성했습니다.

 

MemberRepository 타입의 memberRepository 필드를 선언했습니다. 이는 MemoryMemberRepository 클래스의 인스턴스로 초기화했습니다. 그러면 memberRepository를 통해 MemoryMemberRepository에 정의된 메서드를 호출할 수 있겠죠!

그리고 final은 변수가 한 번 초기화되면 다른 값으로 할당할 수 없음을 나타냅니다.

 


 

1.

회원 가입 메소드를 작성해 보겠습니다.

 

회원을 memberRepository에 저장하기 전에 중복되는 회원이 있는지 검사해야겠죠?

 

memberRepository에서 회원 이름을 통해 중복되는 회원이 있는지 확인한 결과를 Optional result로 반환했습니다.

ifPresent()메소드는 Optional에 값이 존재하는 경우 람다 표현식으로 실행되고 예외를 던지게 됩니다.

IllegalStateException은 프로그램이 현재 상태에서 수행할 수 없는 작업을 시도했을 때 던져지는 예외입니다.

 

이렇게 중복 회원 여부를 검사한 뒤 memberRepository에 해당 회원을 저장하고 id를 반환했습니다.

 

 

그런데 이 코드는 아래와 같이 수정할 수 있습니다.

 

중복되는 result를 없애고 ifPresent 메소드를 이어서 작성한 뒤 함수로 추출했습니다. (ctrl + t / command + option + m)

 


2.

 

전체 회원 조회와 회원 한 명 조회는 간단합니다.

MemoryMemberRepository에 작성해두었던 함수를 사용하면 됩니다.

 


5. 회원 서비스 테스트

 

 

MemoryMemberRepository의 test를 작성할 때는 직접 package와 class를 생성해주었지만

MemberService class에서 command + shift + t 를 통해 자동으로 테스트 클래스를 생성해줄 수 있습니다.

 


1.

회원 가입 테스트

 

연습 단계이기 때문에 테스트를 작성할 때는 given / when / then 을 적어놓고 코드를 작성하는 게 좋다고 합니다!

회원 객체를 생성하고 join 메소드로 회원 가입을 했습니다. (반환값 id였음)

 

생성한 객체의 name과 회원 가입된 객체의 name을 비교하여 회원 가입이 제대로 되었는지 확인했습니다.

 


 

2.

중복 회원 예외 테스트

 

name이 중복되는 회원을 검증하기 위해 member1과 member2에 모두 같은 name을 작성했습니다.

그리고 member1을 회원 가입시킨 뒤 member2도 회원 가입을 하면 예외가 발생해야 하기 때문에 try-catch문으로 우선 작성했습니다.

 

 

try문에서 member2도 중복 회원 가입을 시킨 뒤

fail()메소드를 작성했습니다.fail()메소드는 테스트 프레임워크에서 제공하는 메소드로 테스트를 강제 실패하도록 합니다. (memberSevice.join(member2)에서 예외가 발생하지 않으면 실행되지 않음)즉, 특정 조건이 충족되지 않으면(예외가 발생하면) 명시적으로 테스트를 실패로 표시하는 용도로 사용됩니다.

 

catch 문에서는 예외로 발생하는 e의 메세지가 기존에 작성한 예외 메세지와 같은 지 판단하는 방식으로 예외를 처리한 뒤, 예외 메세지를 검증합니다. (이후 fail 메서드 실행으로 테스트 종료)

 

 

 

하지만 try-catch보다 아래와 같이 작성하는 것이 좋습니다.

 

asserTrows()메서드를 사용했습니다. 이 메서드는 예외가 발생하지 않으면 실패하도록 설계되어 있습니다.

IllegalStateException.class가 있는 부분에는 예상되는 예외 클래스를 작성합니다. 람다 표현식을 사용하여 뒤에는 예외가 발생할 로직을 작성합니다.

이후 assertThat을 이용하여 발생한 예외의 메세지가 기대한 예외의 메세지와 일치하는지 검증합니다.

 

 


 

이 상태로 모든 테스트 코드를 돌리면 MemoryMemberRepositoryTest 때와 마찬가지로 db에 값이 누적되어 에러가 발생합니다.

 

그렇기 때문에 이전처럼 @AfterEach로 db에 있는 데이터를 지워줍니다.

 


 

마지막으로 중요한 문제가 있습니다.

 

 

MemberService와 MemberServiceTest에서 new MemoryMemberRepository로 객체를 생성하는데

두 곳에서 각각 생성하고 있기 때문에 다른 db가 되면서 문제가 발생합니다.

 

 

따라서 아래와 같이 수정합니다.

 

우선 MemberService를 보면 public으로 외부에서 memberRepository를 받아와 this.memberRepository = memberRepository로 사용하게 만들었습니다.

외부에서 가져오는 memberRepository는 바로 MemberServiceTest에서 만들겠죠?

 

MemberServiceTest를 봅시다.

@BeforeEach 어노테이션은 테스트를 생성하기 전에 각각 실행됩니다. (당연히 AfterEach와 반대겠죠. 테스트는 독립적으로 실행되어야 하니까!!)

여기서 new MemoryMemberRepository로 새롭게 memberRepository를 생성하고 이를 MemberService에 넣어줍니다.

그러면 MemberService에서는 여기서 받는 memberRepository를 사용하게 됩니다!!!

 

와우


 

MemberService 입장에서는 외부에서 memberRepository를 받아오게 되는데 이를

Dependency Injection(di)라고 합니다.

 

이와 관련된 자세한 내용은 다음 포스팅에서!!

 


 

command + option + v : extract -> introduce variable

option + enter : show context actions(import class 등)

fn/f6 + shift : 같은 단어 함께 선택

ctrl + t : refactor (extract method : 함수 추출 - command + option + m)

command + shift + t : 해당 클래스의 테스트 생성

ctrl + r : 이전 실행 동일 실행

 

'Spring' 카테고리의 다른 글

스프링 기초4  (0) 2024.02.01
스프링 기초3  (1) 2024.01.28
스프링 기초1  (1) 2024.01.26