회원 관리 예제 작성해보기 - 백엔드 개발
- 비즈니스 요구사항 정리
- 회원 도메인과 레포지토리 제작
- 회원 레포지토리 테스트 케이스 작성
- 회원 서비스 개발
- 회원 서비스 테스트 - Junit이라는 테스트 프레임워크 사용
비즈니스 요구사항 정리
서비스를 개발하기 위해 일단 해당 서비스의 요구사항을 정의 하는 것으로 시작한다.
쉬운 예제로 동작 방식만을 이해하는 것이 목표이므로 간단한 요구사항을 만족해보기로 한다.
비즈니스 요구사항
- 데이터 : 회원 ID, 이름
- 기능 : 회원 등록, 조회
- 데이터베이스는 무엇을 사용할지 지정되지 않았다고 가정한다 (스프링의 특성을 설명하기위해 가상의 시나리오 )
제로 데이터에 특성에 맞게 효율적인 데이터베이스를 선택해야한다. (NoSQL, SQL 등..)
비즈니스 요구사항 정리 단계에서 실제 데이터베이스가 정해지지 않은 상태에서 개발자는 개발을 진행해야 하는 상황으로 생각한다.
시작하기
-
회원 도메인 만들기
비즈니스 요구사항
- 데이터 : 회원 ID, 이름
- 기능 : 회원 등록, 조회
위와 같은 비즈니스 요구사항에 맞추어 도메인과 레포지토리를 작성해 보도록 한다.
먼저 데이터는 회원의 ID(로그인 시 아이디가 아닌 시스템 상에서 식별가능하게 하는 고유 번호이다.) 와 이름이 요구된다.
다음과 같이 hellospring아래에 domain package를 생성해 준다.
그 안에 Member라는 Class를 생성해 주고 그안에 데이터(ID, name)을 만든다.
Member
package hello.hellospring.domain;
public class Member {
private Long id; // serial number
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
데이터는 private 이여야 하며, 이에 접근하기 위해 getter와 setter를 만든다(cmd + N)
시작하기
-
레포지토리 만들기
다음은 레포지토리를 만들어 본다.
위에서 언급한 것 처럼, 아직 데이터베이스와 구현방식이 구체적으로 정해지지 않았기 때문에,
인터페이스를 이용하여 제작한다.
비즈니스 요구사항
- 데이터 : 회원 ID, 이름
- 기능 : 회원 등록, 조회
마찬가지로, repository라는 패키지를 만든다.
그 하위에 인터페이스 형식으로 MemberRepository를 제작한다.
필요로 하는 기능은 회원등록과 조회이다. 그 중, 조회는 ID, Name 두가지 방식과 전체조회 3가지로 구성된다.
MemberRepository
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.List;
import java.util.Optional;
public interface MemberRepository {
Member save(Member member); //회원 등록
Optional<Member> findById(Long id); // 회원 조회 - ID
Optional<Member> findByName(String name); // 회원 조회 - Name
List<Member> findAll();
}
MemoryMemberRepository
이는 MemberRepository 인터페이스를 implements 했기 때문에,
인터페이스에 존재하는 함수는 전부 오버라이드 해서 구현해야한다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.lang.reflect.Array;
import java.util.*;
public class MemoryMemberRepository implements MemberRepository{
private static Map<Long, Member> store = new HashMap<>(); // 데이터를 저장할 Map 자료형
private static long sequence = 0L; // ID용
@Override
public Member save(Member member) {
member.setId(++sequence); //ID setting
store.put(member.getId(),member); // Map에 저장
return member;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id)); //결과가 없다면
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream().filter(member -> member.getName().equals(name)).findAny();
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
}
인터페이스(MemberRepo...)를 implements 해와서 구현해준다.
*tip : implements MemberRepository 작성 후 option + enter 를 눌러 전체를 불러온 후 구현해주면 편하다.
option + enter는 무언가 빨간줄(즉, 임포트를 하지 않았거나 등으로 생기는 에러)를 바로 해결해주는 단축키
이제 구현이 끝났다.
하지만 MemoryMemberRepository부분은 복잡하니 하나하나 설명하기로 한다.
사용된 변수
먼저 데이터를 저장해둘 임시 store를 HashMap형태로 생성한다.
<key, values> <ID, member>로 각각 저장하며,
sequence변수는 ID를 하나씩 올려 넣어줄 변수이다.
private static Map<Long, Member> store = new HashMap<>(); // 데이터를 저장할 Map 자료형
private static long sequence = 0L; // ID용
*실무에서는 동시성 문제로 공유되는 변수에 대해서는 단순한 HashMap을 사용하지 않고, concurrent HashMap을 사용해야한다.
long또한 마찬가지 일반 long형태를 사용하지 않는다.
public Member save(Member member)
새로운 member가 추가되는 함수이다.
지금은 데이터베이스가 존재하지 않으니, Map자료형으로 만들어둔 store에 데이터를 저장하도록 만든다.
@Override
public Member save(Member member) {
member.setId(++sequence); //ID setting
store.put(member.getId(),member); // Map에 저장
return member;
}
Member 클래스와 시스템상에서 식별용으로 사용하는 ID는 사용자에게 입력받는 것이 아닌
시스템상에서 식별이 가능해야하므로, 자체적으로 sequence라는 변수를 만들고, 새로운 member가 추가될 때 마다 +1을 하여 ID를 만든다.(setter사용)
store변수에 만든 ID와 생성된 Member객체를 넣어 저장한다.
public Optional<Member> findById(Long id)
id를 입력받으면, 해당 id에 해당하는 Member를 반환해주는 함수
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id)); //결과가 없다면
}
일단 id를 기반으로 store에서 데이터를 꺼낸다.
return store.get(id)
그런데, 결과가 NULL일 경우, Optional으로 감싸주면 Null이 반환되어도 클라이언트 단에서 처리하기 용이하다
Optional.ofNullable(<null이 반환될 가능성이 있는 함수>);
다음과 같이 감싸주면 된다.
public Optional<Member> findByName(String name)
name을 입력받으면, 해당 name을 가진 Member를 반환해주는 함수
public Optional<Member> findByName(String name) {
return store.values().stream().filter(member -> member.getName().equals(name)).findAny();
}
복잡한데 차례대로 보자
.stream()
스트림이라는 개념을 여기서 다 정리하기는 양이 너무 많다.
간단하게 설명하면, 많은 수의 데이터를 다룰 때 컬렉션, 배열에 데이터를 담아 두고 사용하는데 이때 데이터 조회 등에 for, Iterator등을 사용했다.
이를 더 간결하게 사용하기 위해 Stream이라는 개념이 생겨났다.
store에 담긴 values들(member)들을 기반으로 스트림을 생성한다.
*Map은 <key, values> 형태 !
store.values().stream()
이제 이 데이터들을 스트림을 이용하여, 더욱 간결하게 사용할 수 있게 되었다.
Stream<T> filter(Predicate<T> predicate)
다음은 생성된 stream의 filter함수이다. 조건에 안 맞는 요소를 제외시키는 함수이다.
store.values().stream().filter(<조건>).
즉, stream에서 조건에 맞지 않는 내용을 제외한다. (==조건에 맞는 내용만을 반환한다)
#스트림 #Stream
이제 조건을 한번 보면, 다음과 같이 구성되어 있다.
member -> member.getName().equals(name)
이는 JAVA 8에서 도입된 람다식이다. (화살표함수)
이에 대한 자세한 설명은 하지 않고 간단하게 말하면, 다음과 같다. 이 식을 method로 생각하자
화살표 이전은 method로 넘어온 매개변수이고, 화살표 이후의 값은 해당 method의 return값이다.
member에서 getter를 이용해 name에 접근 한 후 파라미터로 넘어온 name과 동일한 경우 return해준다.
#람다식
.findAny()
결과가 1개일 지라도 찾아냄
즉, 전체적으로 보면 loop를 돌면서, 하나가 찾아지면 반환하고
끝까지 돌았는데 없으면 Optional을 이용해 반환한다.
public List<Member> findAll()
저장되어 있는 Member를 전체조회하는 함수
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
store내에 있는 member(values)를 리스트 형태로 반환한다.
이제 완성되었다. 작성된 것들이 제대로 동작하는지는 검증을 통해 알아볼 수 있다.
바로 테스트 케이스를 작성하는 것이다.
'Backend > Spring' 카테고리의 다른 글
[Spring] 회원관리 예제 - 3) 회원 서비스 (0) | 2021.09.07 |
---|---|
[Spring] 회원관리 예제 - 2) 테스트 케이스 작성(검증) (0) | 2021.09.07 |
[Spring] 웹개발 기초(정적,MVC,API) (0) | 2021.09.01 |
[Spring]정적, 동적 페이지 동작원리 (0) | 2021.08.31 |
[Spring] 스프링부트 활용하기 (0) | 2021.08.31 |