-
[스프링 부트 Step7] - 웹 앱 구조와 테스트 케이스 본문
이제부터 실습에 들어가보려 합니다.
그래서 몇 가지 고객의 정리된 요구사항을 보며 어떻게 개발을 할지 생각해봅시다.
- 회원 도메인과 레포지토리 만들기
- 회원 레포지토리 테스트 케이스 작성
- 회원 서비스 개발
- 회원 서비스 테스트
비즈니스 요구사항은 가장 쉬운거로 해볼 것입니다.
데이터 : 회원 ID, 이름
기능 : 회원 등록, 회원 조회 만으로 해보겠습니다.
저희는 아직 데이터 저장소를 선정하지 않았다고 해봅시다.
성능이 중요한 DB를 쓸지, 아니면 관계형 데이터베이스인 RDBMS로 할지를 안정해져있는데 개발을 해야한다면 어떻게 할지를 특정합니다.
일반적으로 웹 애플리케이션의 계층 구조는 다음과 같습니다.
그림에서 등장하는 컨트롤러는 웹 MVC와 컨트롤러의 역할을 합니다. 지금까지 쭉 다뤄왔던 내용입니다.
서비스는 서비스 클래스의 핵심 비즈니스 로직이 이곳에 들어갑니다. 예를 들면 회원은 중복 가입이 안된다거나 중복해서 게시글을 삭제할 수 없다는 내용의 기본 로직이 들어갑니다.
그리고 도메인은 회원, 주문, 쿠폰처럼 데이터베이스에 주로 저장하고 관리하는 비즈니스 도메인 객체입니다.
레포지토리는 비즈니스 도메인 객체를 가지고 핵심 비즈니스 로직을 구현하도록 동작하는 것이라고 합니다.
보통 서비스 클래스를 이름을 지을 땐 위와 같이 MemberService, BoardService 등 특정 기능에 서비스를 붙여서 만듭니다.
그리고 레포짙리는 주로 인터페이스로 설계를 하는데요.
이유는 DB를 아직 설정하지 않았기 떄문에 하단의 메모리멤버레포지토리를 금방 만들어 향후에 DB를 선정 후 바꾸어서 사용할 것이기 때문에 인터페이스도 정의를 했습니다.
자 그럼 본격적으로 회원 도메인과 레포지토리를 만들어 봅시다!
회원 도메인과 레포지토리 만들기
우선은 controller 밑에 domain이라는 패키지를 하나 생성합니다.
그리고 위와 같이 domain내부에 Member라는 클래스를 만듭니다.
도메인은 기본적으로 식별자가 들어가는 곳입니다.
(그리고 패키지는 소문자, 클래스는 대문자로 시작하는건 예전에도 설명드렸죠?)
그리고 이와 같이 입력합니다.
id는 서버에서 자동 생성하여 만드는 것이므로 Long타입으로 선언합니다.
그리고 우리는 API방식으로 데이터를 주고 받을 것이기 때문에 ALT + Insert키를 눌러서 Getter,Setter세팅을 합니다.
이후 레포지토리 패키지도 생성합니다.
그리고 그 내부엔
MemberRepository라는 인터페이스를 생성했습니다.
자바 클래스로 생성해서 밑에 인터페이스 클릭하면 인터페이스로 생성 가능합니다.
우리는 여기를 저장소로 사용할 것입니다.
그리고 이와같이 입력합니다.
save한다는 것은 Member객체를 save한다는 것입니다.
Optional<Member> findById(Long id); 라는 것은 Member의 Long타입의 id로 회원을 찾겠다는 뜻을 의미합니다.
또한 Optional<Member> findByName(String name); 이라는 것은 멤버의 name으로 회원을 찾게다는 의미입니다.
그럼 Optional은 무엇일까요?
이 Optional은 자바 8에 도입된 기능입니다.
우리가 findById와 findByname을 했는데 값이 null일 수도 있습니다.
옛날에는 없으면 그대로 저것들을 null로 반환했습니다.
하지만 요즘은 Optinoal이라는 것으로 감싸서 null을 반환하는 기능을 지원합니다.
그리고 마지막으로 findAll하면 Member를 List에 담아서 모두 반환해줍니다.
자 그럼 구현체를 만들어서 실체를 확인해봐야겠죠?
위와 같이 클래스 타입으로 MemoryMemberRepository를 생성해봅시다.
그리고
이렇게 입력하시면 인터페이스인 MemberRepository를 상속합니다.
그럼 아마 쭉 빨간줄이 뜨실텐데 그때 Alt + Enter누르면 오버라이딩하라고 어떤 창이 뜹니다.
confirm하시면
이렇게 뜹니다.
자 구체적으로 구현해봅시다.
제일 처음 save를 할 때 어느 메모리에 저장을 해야할 것입니다.
Map이라는 자료구조를 써보겠습니다.
import해주시고 store라고 map이름을 짓습니다.
실무에서는 동시성 문제가 있을 수 있기 때문에 공유되는 변수일때는 ConcurrentHashMap()을 써야하는데 우리는 예제를 하는 것이기 때문에 HashMap을 쓰겠습니다.
본문 내용을 살펴보면 HashMap<Long, Member> store 이렇게 선언해도 되는데 굳이 Map을 사용했습니다.
그 이유는 향후 Map을 변경할 때 HashMap이라는 제약을 걸지 않고 Map의 제약을 따르겠다는 것이 이유입니다.
즉, HashMap<Long, Member> store 이렇게 사용해도 되지만 다른 개발자들이 사용할 때 변경이 용이한 것은 HashMap이 아닌 Map<Long, Member> store = new HashMap<>(); 이기 때문에 이렇게 작성한 것입니다.
만약 정말 HashMap의 구체적인 기능을 사용해야 한다면 HashMap store = new HashMap()이라고 선언해도 무방합니다.
그리고 밑의 sequence는 0,1,2 등 key값을 생성해줍니다.
실무에서 사용할 때는 역시나 동시성 문제를 고려해야 합니다.
이렇게 작성해봅니다.
member의 Id값을 set하겠다는 건데, sequence를 1올려서 저장한단 뜻입니다.
그리고 store에는 아까 봤듯이 Map으로 선언했습니다.
첫 번째 들어가는 member.getId()가 Key값에 해당하고 member가 Value값에 해당합니다.
이미 setId하면서 member의 id를 지정했기 떄문에 member.getId()를 통해 member의 id를 넣을 수 있습니다.
그리고 저장된 결과를 반환해주면 끝납니다.
findById는 store에서 id를 찾아서 꺼내오면 됩니다.
그런데 결과가 없을 수도 있잖아요?
과거에는 그냥 null을 보냈는데, 요즘은 Optional로 감싸면 null이 들어와도 그대로 감싸서 반환할 수 있게 해줍니다.
store.values().stream() 이렇게 입력하시면 store의 value값들을 stream 즉, 돌리겠단 뜻입니다.
무작정 모든 것들을 돌릴 수 없으니 filter가 필요합니다.
그래서 .filter라고 써주시고 람다가 사용되는데요.
member에서 member.getName().euqals(name))이렇게 입력하시면 member에서 넘어온 member의 name이 서로 같은지를 확인합니다.
만약 찾으면 반환을 해야하므로 findAny()라고 입력합니다.
findAny는 그냥 하나라도 찾으면~ 이렇게 이해하시면 좋겠습니다.
이를 통해 Map을 돌면서 name을 찾으면 그대로 반환할테고 없으면 Optional에 null이 포함되서 반환됩니다.
실무에선 ArrayList의 사용에 익숙해지셔야 겠습니다.
List의 모든 Member를 찾을텐데, store의 values들을 모두 찾아서 return하면 끝입니다.
자 이렇게 멋지게 작성을 했는데 이게 제대로 동작하는지 안하는지 모를 수 있습니다.
그럴땐 필히 테스트 케이스를 작성하셔야 합니다.
자 한 번 보겠습니다.
보통 개발한 기능을 실행해서 테스트를 할 때 main()메서드를 통해서 실행하거나, 웹 애플리케이션의 컨트롤러를 통해서 해당 기능을 실행합니다.
사실 이러한 방법은 준비하고 실행하는데 오래 걸리고 반복 실행하기 어려우며 여러 테스트를 한 번에 실행하기 어렵다는 단점이 존재할 수 있습니다.
따라서 자바에서는 제일 첨에도 얘기드렸다시피 JUnit이라는 프레임워크를 지원하는데, 이를 이용하여 문제를 해결해보려고 합니다.
보통 테스트는 클래스와 똑같이 이름을 짓고 뒤에 Test를 붙이는 것이 관례입니다.
다 작성했으면 이렇게 한 번 작성해봅니다.
MemberRepository의 생성자 repository라고 만듭니다.
이후 save()기능이 동작하는지 보는 것이기 때문에 save()메서드 위에 @Test를 걸어줍니다.
그럼 이제부터 이 save()를 가상으로 실험해볼 수 있습니다.
Member의 생성자 member를 만들고 member의 Name을 "jeen"이라고 지어볼게요.
이후 repository에 member를 save함으로써 repository에 member를 넣습니다.
그리고 repository에서 name을 찾을 것이므로 repository.findById라고 입력한 뒤 member.getId()하면 member의 ID를 찾아서 그것에 .get()을 달아주면 id를 얻어옵니다.
근데 그냥 얻어오면 안되므로 객체 지향인 자바를 더 잘 이용하기 위해 Member의 result라는 것에 담아옵니다.
그리고 그 결과를 확인하기 위해 System.out.println("result = " + (result == member)); 라고 입력하면 result와 member가 같은지를 true, false로 판단할 수 있긴 합니다.
하지만 좀 더 확실하게 알고싶잖아요?
그래서 Assertions.assertEquals(member, result);라고 입력하시면 member와 result가 같으면 알려줍니다.
그리고 save메서드를 실행 누르면 이와 같이 실행될 것입니다.
만약
이렇게 실행하면 어떨까요?
당연히 실제 값과 null은 다르기 때문에 test가 fail 될 것입니다.
이것 외에도 요즘은 이걸 많이 씁니다.
이렇게 입력하여 member와 result를 비교하는 방법도 실무에서 굉장히 많이 쓰입니다.
다음 포스팅에서도 Test를 계속 해보겠습니다.
너무 길어져서 한 번 끊어야겠어요.
감사합니다.
'백엔드 기술 정리 > 스프링 부트' 카테고리의 다른 글
[스프링부트 Step 9] - 회원 서비스 개발 (0) | 2021.12.27 |
---|---|
[스프링 부트 Step 8] - 회원 레포지토리 테스트 케이스 (0) | 2021.12.27 |
[스프링부트 Step6] - API란? (0) | 2021.12.23 |
[스프링 부트 Step5] - MVC와 템플릿 엔진 (0) | 2021.12.18 |
[스프링 부트 Step4] - 빌드와 배포 그리고 스프링 웹 개발 기초 (0) | 2021.12.11 |