이 글은 인프런에 있는 김영한님의 "스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술" 강의를 듣고 정리한 필기입니다.
⛅️ 회원 도메인 만들기
먼저 domain이라는 package를 만들고 Member 클래스를 생성하자.
이 클래스에는 회원의 정보를 담을 변수들을 선언한 것이다.
이전 시간에 살펴보았던 비즈니스 요구사항을 보면 회원에 필요한 데이터는 '회원 ID'와 '이름'이었다.
여기서 회원 ID는 회원이 직접 적는 값이 아니라 데이터 구분을 위해 시스템이 정한 ID이다. Long 타입으로 변수 id를 선언해준다.
이름은 회원이 직접 정하는 이름이다. 문자열 변수 String name을 선언한다.
이후 Getter/Setter를 생성해준다.
(Getter/Setter가 좋냐 별로냐에 대한 의견은 있지만, 이 예제는 단순하기 때문에 Getter/Setter를 이용하자.)
public class Member {
private Long id;
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; }
}
⛅️ 회원 리포지트리 인터페이스 만들기
먼저 repository라는 package를 하나 생성한다. repository는 저장소라는 뜻인데, 이 예제에서는 회원 객체를 저장하는 저장소를 만들 것이다.
이후 인터페이스 MemberRepository를 생성한다.
interface는 함수의 구현부가 없고, 추상 메서드와 상수만을 가질 수 있다.
또한 interface를 상속받는 클래스에게 함수 구현을 강제하여 반드시 구현해야하는 기능을 구현할 수 있고,
여러 클래스가 해당 인터페이스를 상속받았을 때, 인터페이스만을 수정하여 개발 코드의 수정을 줄여 유지 보수성을 높일 수 있다.
주어진 시나리오가 현재 데이터 베이스(DB)가 선정되지 않은 상황이기 때문에 우선 메모리 기반의 데이터 저장소를 사용하기로 했다.
만일 메모리 기반 데이터 저장소 클래스에 모든 내용을 구현하는 것도 나쁘지 않겠지만, 만일 데이터 저장소를 선정하고 해당 데이터 저장소로 기능들을 옮기려면 메모리 기반 데이터 저장소의 코드를 모두 직접 옮겨야 할 것이다.
이 과정에서 빠트리는 기능이 생길 가능성도 있다.
하지만 interface로 필요한 기능들을 선언하고, 이를 상속받아 사용한다면 저장소 교체가 더욱 수월할 것이다.
그럼 이제 필요한 기능들을 만들어보자.
public interface MemberRepository {
Member save(Member member); // 1. 저장
Optional<Member> findById(Long id); // 2. 시스템이 인식하는 ID를 통해서 member 찾기
Optional<Member> findByName(String name); // 3. 사용자 이름을 통해 member 찾기
List<Member> findAll(); // 4. 저장된 모든 회원 리스트 반환
}
- Save (저장)
- member에 들어갈 정보(id, name)을 받았을 때, 이를 저장하는 기능 - findById
- 시스템이 인식하는 id를 받아서 해당 id를 갖고 있는 member를 찾는 기능 - findByName
- 사용자 이름을 받아서, 해당 이름을 갖고 있는 member를 찾는 기능 - findAll
- 지금까지 저장된 모든 회원 리스트를 반환하는 기능
⛅️ 구현체 만들기
구현체가 상속할 interface를 만들었으니, 이제 구현체를 만들어보자.
repository package 안에 MemoryMemberRepository 클래스를 생성하고, implements를 통해 위에서 선언한 interface를 상속받는다.
그러면 오류가 날텐데 이는 인터페이스에서 선언한 메서드들을 오버라이드하지 않았기 때문이다.
Alt+Enter를 누르고 Implemets methods를 선택한다. 그럼 다음과 같은 상태가 될 것이다.
public class MemoryMemberRepository implements MemberRepository{
@Override
public Member save(Member member) { }
@Override
public Optional<Member> findById(Long id) { }
@Override
public Optional<Member> findByName(String name) { }
@Override
public List<Member> findAll() { }
}
메서드들을 구현하기 전에 우선 필요한 멤버 변수들을 선언해보자.
private static Map<Long, Member> store = new HashMap<>(); // member 객체 저장
private static long sequence = 0L; // 시스템이 부여하는 ID 번호
member들의 정보를 save할 때 저장할 장소가 필요할 것이다. 메모리 기반 저장소를 사용할 예정이므로, 이번 예제에서는 Map을 사용해보자.
또한 시스템이 인식하는 id가 있다고 했는데, 회원의 정보를 저장할 때 id를 부여해야 할 것이다. 그러므로 long형 변수를 선언하고, 새로운 멤버 객체를 저장할 때마다 값을 하나씩 증가시켜서 id를 부여하자.
이제 오버라이드된 메서드들을 구현해보자.
1. save
@Override
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
save는 Map에 member 객체를 저장하고, 저장한 객체를 반환하는 함수이다.
member 객체의 변수에는 id와 name 두 개가 있었는데, name은 사용자에게 이미 입력을 받은 상태라고 가정하자.
우선 ++sequence를 하여 id 값을 설정 -> member의 프로퍼티인 setId를 통해 해당 멤버의 id를 설정 -> 현재 member의 id와 member 객체를 pair 객체로 하여 Map에 삽입 -> member 객체 반환한다.
2. findById
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
시스템이 구별하는 id를 통해 member 객체를 찾는 메서드이다.
여기서 눈 여겨 볼 것은 Optional이다. HashMap에서 id(key)에 해당하는 member 객체(value)를 반환하는데, 만일 해당하는 id가 없을 수 있다.
Optional은 객체를 포장해주는 Wrapper Class인데, 위의 경우에서 null이 될 가능성이 있기 때문에 Null Pointer Exception(이하 NPE)이 발생할 수 있다. 하지만 Optional.ofNullable로 감싸주게 되면 빈 Optional 객체를 반환하여 NPE가 발생하는 것을 막을 수 있다.
이렇게 Optional로 감싸서 반환을 해주면 클라이언트에서 처리를 할 수 있다. 이건 추후 뒤에서 설명할 예정이다.
3. findByName
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
이 함수는 문자열 name을 받아서 HashMap에서 해당 name인 member 객체를 반환하는 기능이다.
stream은 배열 또는 컬렉션 인스턴스를 다룰 수 있는 방법인데, java 8부터 지원하는 이 기능을 사용하여 구현해보자.
store.values().stream()는 store(HashMap)의 value들. 즉, member 객체를 돌면서 확인한다는 뜻이다.
.filter(member -> member.getName().equals(name)은 member.getName이 parameter로 받은 name과 같은지 확인하고, 같은 경우에만 필터링을 한다.
.findAny()는 필터링한 결과가 하나라도 있다면, 그 결과를 Optional로 반환을 한다. 만일 값이 없다면 Optional에 null이 포함되서 반환이 된다.
4. findAll
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
findAll은 지금까지 저장한 모든 member 객체들을 List로 반환하는 메서드이다.
여기서 확인할 것은 우리가 member 객체들을 id라는 key에 물려서 HashMap에 저장했는데, 반환값은 List라는 것이다.
따라서 새로운 ArrayList를 생성하고, 그 안에 store.values(). 즉, HashMap의 모든 value들(member 객체겠죠?)을 이 ArrayList에 담아서 반환할 것이다.
다음 시간에는 구현한 기능들이 제대로 작동하는지 확인해보자!