Spring/Spring boot 입문

[스프링 부트] 회원 관리 예제(1)~(3) - 스프링 입문 강의 / 인프런

mopipi 2022. 7. 30. 18:17
반응형

4. 회원 관리 예제 - 백엔드 개발


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


비즈니스 요구사항 정리

  • 데이터 : 회원 ID, 이름
  • 기능 : 회원 등록, 조회
  • 아직 DB가 선정되지 않았음을 가정하고 개발

 

일반적인 웹 애플리케이션 계층 구조

출처 : 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 (김영한)

  • 컨트롤러: 웹 MVC의 컨트롤러 역할, API 생성 시 컨트롤러 역할
  • 서비스 : 서비스 클래스의 핵심 비즈니스 로직이 들어가 있음 (ex. 회원 가입 중복 불가)
  • 도메인 : DB에 주로 저장되고 관리되는 (ex. 회원, 주문, 쿠폰 ...) 비즈니스 도메인 객체 
  • 리포지토리 : DB에 접근, 도메인 객체를 DB에 저장하고 관리
    • 비즈니스 도메인 객체를 가지고 핵심 비즈니스 로직이 돌아가도록 구현한 계층

클래스 의존관계

출처 : 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 (김영한)

  • 아직 DB가 선정되지 않음 → 인터페이스로 설계 (회원 리포지토리에) → 메모리 구현체로 생성
    • 메모리 구현체 : 비교적 간단하게 생성 가능, 메모리에 삽입 삭제 가능 (가벼운 메모리 기반 데이터 저장소)
      • 구현체 = 인터페이스를 구현한 클래스
      • [interface MemberRepository] -- [class MemoryMemberRepository implements MemberRepository] 가 있을 때, MemoryMemberRepository 는 MemberRepository 인터페이스를 구현한 구현체
    • 추후에 DB가 선정되면 바꿔끼우기 위해 인터페이스가 필요함 (구현 클래스 변경 가능)
      • 확장에 대한 요구사항 (변경에 닫혀있고, 확장엔 열려있게 설계)  → 객체간 느슨한 결합 필요 ⇒ 인터페이스

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


1. 회원 객체 생성

 

domain 패키지 생성 -> Member 클래스 정의

- 맴버 변수로 id, name 선언

- 이때 id는 시스템에서 데이터 식별 위해 임의로 부여하는 숫자

 

2. 회원 리포지토리 인터페이스 생성

 

4가지 기능 구현 in repository 

  1. save : 회원을 저장소에 저장하는 기능. 
  2. findById : save로 저장된 회원 정보를 id로 찾아서 반환하는 기능
  3. findByName : save로 저장된 회원 정보를 name으로 찾아서 반환하는 기능
  4. findAll : 지금까지 저장된 회원 리스트를 전부 반환

Optional

  • Java 8에 들어가는 기능
  • null 반환 시, null을 반환하는 방식 중 하나
  • null을 그대로 반환하는게 아닌, 포장해서 반환하는 기능

 

3. 회원 리포지토리 메모리 구현체 생성

Alt + Shift + Enter를 통해 implements가 손쉽게 가능하다.

 

실무에서는 동시성 문제가 발생 가능함
   ➔ 공유 되는 변수일 경우 HashMap 대신 ConcurrentHashMap 사용해야 함
   ➔ 마찬가지로 sequence에서 long 대신 atumnlong 등으로 대체 필요

Optional.ofNullable

     - null값이 return될 경우 감싸서 반환 해줌 Client에서 뭐 해줌(?)

 

Stream 생성 → 필터 (조건에 부합하는 데이터 거름) → 찾음 (findAny() = 병렬처리 때 먼저 찾아진 요소 반환)

    ⇒ 하나 찾는 순간 바로 반환 , (if: 다 돌렸는데도 일치하는게 없음 = Optional 에 null이 포함 돼 반환)

 

(+ Stream이 익숙치 않아 추가적으로 알아보았다 >> https://c-omealong.tistory.com/23)

♘Lambda
    - 형식 : 인터페이스명 메소드명 = (파라미터 명) -> { 수행할 코드 }
    - 메소드를 정의하지 않고도 메소드처럼 활용 가능
    - 주로 일회성으로 사용할 경우
  

- 생성은 Map이지만 반환은 List로 함 (∵ 실무에는 List가 사용 多)

반응형

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


✔ 생성한 Member Repository 클래스가 내가 원하는대로 정상 작동 하는지 검증

    → 테스트 케이스 작성해서 검증 (코드를 코드로 검증)

회원 리포지토리 메모리 구현체 테스트

 - 개발한 기능 테스트 : 자바의 main 메서드 or 웹 어플리케이션 컨트롤러 통해 해당 기능 실행하며 테스트

    → 실행 소요시간 큼 + 반복 실행 어려움 + 여러 테스트 한번에 실행 어려움

    ➔ JUnit (프레임워크)로 테스트 코드 만들어서 테스트 코드 자체를 실행함으로써 해결 가능 (in JAVA)

 

test/java/practice~/ 에 "테스트 할 패키지와 동일한 이름"+Test로 패키지, 클래스 생성

+) 굳이 public 설정할 필요 없음

 

Ⅰ) save 메서드 정상 동작 테스트

  • org.junit.jupiter.api.Test을 import 해주면 save 메서드 실행 가능 → 동작 테스트 가능

1. 테스트 할 save 메서드, 임의의 회원 객체 생성

정상 동작하는 것을 확인

코드 자동완성 단축기 = Ctrl + Shift + Enter

✔ 이때, returnType (반환 타입)이 Optional임 → Optional에서 get메서드 이용해 값 꺼내 result에 저장

 

2. 검증하기 -  (1) println() 이용

  - new Member로 생성하면서 저장한 데이터와 /  DB에서 꺼내온 데이터가 동일한지 비교한 결과 확인

      ➔ 가져온 result와 member가 같은 경우 (정상 작동)  True 출력

 But 일일히 확인하기란 사실상 불가능하다 

 

2. 검증하기 - (2) Assertions 이용 [ org.junit.jupiter의 api ]

Assertions.assertEquals("Expected", "Actual");

 

  • Assertions (junit)의 assertEqual 메서드를 사용
    • Expected : 기대하는 것 (비교할 기준)
    • Actual : 실제 비교할 값

∴ new Member()로 생성해 저장한 객체(Expected) == findById 로 찾은 객체 (Actual)

 

만약 비교 대상이 같지 않다면 오류가 이렇게 표시된다 (서로 다른 Expected, Actual을 보여준다)

2. 검증하기 - (3) Assertions 이용 [ org.assertj.core의 api ]

Assertions.assertThat("Actual").isEqualTo("Expected");
  • Assertions (assertj)의 assertThat(A).isEqualTo(B) 메서드 이용
    • A가 B랑 일치하는지 확인  ∴ A = 비교할 것 / B = 비교 대상

 

✔  Alt + Enter ⇒ static import 가능 (이후 assertThat만 바로 이용 가능)

==> 빌드 툴에서 빌드시, 테스트 케이스를 통과하지 않으면 다음단계로 넘어가는 것이 방지됨

 

Ⅱ) findByName 메서드 정상 동작 테스트

 

1. 테스트 할 findByName 메서드 생성

2. 비교할 객체 member1, member2 생성 후 name할당 → repository에 저장

Rename 단축키 = Shift + F6

 

3. 검증하기 - assertion.assertThat() 

result(="spring1"이 name인 member) 과 member1이 동일

 

 만약 비교 대상이 같지 않다면("spring2"로 findByName 실행하므로 member2 저장)

result(=member2) =/= member1

⁂ Test case의 장점
  - (1) save, (2) findById 를 동시에 테스트 가능 : 한번에 여러 메서드 동시에 테스트 가능
   (Test클래스 레벨에서 Run하거나, Run Tests in practice~ 에서 실행하면 전체 class 테스트 가능)
test/.../ repository 패키지 우클릭 > Run Tests~ 실행 시 하위 클래스 한번에 테스트 진행
한꺼번에 테스트가 가능하다
반응형

Ⅲ) findAll 메서드 정상 동작 테스트

 

저장된 전체 회원객체를 불러와 List에 저장하고, 그 길이를 비교함으로써 검증 가능

 

기준 변수 추출 단축키 = Ctrl + Alt + v

But 다시 테스트 클래스에 대해 Run 할 경우 에러 발생

 ➔ member의 name이 중복 되기 때문! ("spring1", "spring2")

 

테스트 메서드는 순서 보장 X (∵ 모든 메서드 케이스는 생성 순서 상관없이 메서드 별로 병렬 수행 처리됨)

각 메서드 테스트 실행 후 repository를 주기적으로 초기화 시켜줄 메서드인 AfterEach에 대해 정의해야 함!
 (Ex. findAll() >> AfterEach() >> findByName() >> AfterEach() >> save() 순서대로 실행)
+ MemoryMemberRepository 클래스에 clearStore 메서드 정의 ⇒ 메모리(store) 초기화 기능 추가

이후 실행 시켜주면 오류 없이 작동한다. 

 

♘ 테스트 케이스 작성시 유의할 점
    - 테스트는 서로 의존관계없이 설계돼야 함
    ∴ 테스트가 끝날 때마다 저장소 or 전역 변수 (=공유 데이터)를 초기화 해주는 메서드 필수

+)

  • 테스트 케이스(=틀)을 먼저 생성한 뒤, 거기에 맞는지 테스트 해가며 진행하는 개발 방식도 존재
    • 테스트 주도 개발 (TDD) : [선] 테스트 → [후] 구현 클래스 생성 
  • 테스트 개수가 많을 경우
    • 빌드 or 최상위 레벨에서 Run Tests... 실행 or gradle 
반응형