(인프런) 김영한님의 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술을 공부하고 리뷰한 글입니다.
1. 회원 도메인과 리포지토리 만들기
1. 회원 객체
1) domain 패키지 생성
2) domain 패키지에 Member 클래스 생성
3) 회원 객체(Member) 코드 작성
package hello.hellospring.domain;
public class Member {
private Long id; // 단순히 데이터 구분을 위해 사용(실제 회원 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;
}
}
외부에서 값을 바로 접근할 수 없도록 id, name을 private로 선언하고 각각 Getter, Setter 메서드를이용하여 외부에서 값을 바꾸거나 가져올 수 있도록 한다.
2. 회원 리포지토리 인터페이스
이전 포스팅에서 말했듯이 DB가 아직 정해져있지 않은 상황을 가정하고 개발해야하기 때문에 인터페이스를 만들고 진짜 구현은 3번에서와 같이 회원 리포지토리 메모리 구현체를 만들어 구현한다.
1) repository 패키지 생성
2) repository 패키지에 MemberRepository 인터페이스 생성
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); // 회원 객체(id, name) 저장
Optional<Member> findById(Long id); // 회원 id 조회
Optional<Member> findByName(String name); // 회원 이름 조회
List<Member> findAll(); // 저장된 모든 회원 리스트 조회
findById(), findByName()을 보면 반환 타입이 Optional<Member>이다.
Member 객체를 그냥 반환해도 되지만 파라미터로 들어온 id나 이름에 해당하는 회원이 없어 null을 반환해야할 경우에 if문으로 값이 null인지 null이 아닌지 검사해주는 코드를 매번 써야하는 번거로움이 있다. 이를 해결하기 위해 Optional<T> 클래스를 사용하였다.
<Optional 이란?>
Java8에서는 Optional<T> 클래스를 사용해 NPE(NullPointerException)을 방지할 수 있도록 도와준다.
Optional<T> 클래스는 Integer나 Double 클래스처럼 'T'타입의 객체를 포장해 주는 래퍼 클래스(Wrapper class)입니다. 따라서 Optional 인스턴스는 모든 타입의 참조 변수를 저장할 수 있습니다.
이러한 Optional 객체를 사용하면 예상치 못한 NullPointerException 예외를 제공되는 메소드로 간단히 회피할 수 있습니다.
즉, 복잡한 조건문 없이도 널(null) 값으로 인해 발생하는 예외를 처리할 수 있게 됩니다.
더 자세한 설명은 Java8의 Optional이란? 에서 확인할 수 있습니다.
3. 회원 리포지토리 메모리 구현체
2번에서 생성한 회원 리포지토리 인터페이스를 구현한 회원 리포지토리 메모리 구현체를 만드는 부분이다.
1) 위에서 생성한 repository 패키지에 MemoryMemberRepository 클래스 생성
2) 회원 리포지토리 메모리 구현체 코드 작성
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.*;
/**
* 동시성 문제가 고려되어 있지 않음, 실무에서는 ConcurrentHashMap, AtomicLong 사용 고려
*/
public class MemoryMemberRepository implements MemberRepository {
// Map<key: Member의 id, value: Member>
private static Map<Long, Member> store = new HashMap<>(); // 회원 저장 공간
private static long sequence = 0L; // 시스템에서 Member의 id를 자동으로 넣어주기 위함
// (id, member)를 메모리에 저장하고 member를 반환하는 함수
@Override
public Member save(Member member) {
member.setId(++sequence); // id 값을 자동으로 1개 증가
store.put(member.getId(), member); // (member의 id, member)를 map에 넣음
return member;
}
// key가 id인 member를 반환하는 함수
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id)); // member가 null일 수도 있으니 optional로 감싸기
}
// 메모리에 저장된 모든 member(value)들을 반환하는 함수
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values()); // store의 member들 반환
}
// 메모리에 저장된 회원들 중 파라미터(name)과 같은 member의 name을 가진 member를 반환하는 함수
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
// 메모리에 저장된 모든 회원 정보(id, member)를 삭제하는 함수
public void clearStore() {
store.clear();
}
}
MAP, HashMap 정보 >> Map이란? HashMap이란? 글을 참고하세요.
Stream 관련 정보 >> Java8의 Stream 이란? 글을 참고하세요.
<Stream 관련 부분>
// 메모리에 저장된 회원들 중 파라미터(name)과 같은 member의 name을 가진 member를 반환하는 함수
@Override
public Optional<Member> findByName(String name) {
// 데이터소스객체집합.steam생성.중개연산.최종연산
// stream에서 filter(조건)을 만족하는 어떤 요소 반환
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
- filter(조건): 조건을 만족하는 Stream 리턴
- findAny(): Stream 중 어떤 요소 리턴
Q. Map<Long, Member>로 HashMap을 생성하는 이유는?
Map은 리스트나 배열처럼 순차적으로(sequential) 해당 요소 값을 구하지 않고 key를 통해 value를 얻는다. (키는 중복X, value는 중복O) 여기서 key는 Member의 id이고 value는 Member객체이다.
나도 처음에는 id값과 Member(id, name) 이렇게 저장하는게 맞는지에 대한 의문이 들었다. 이렇게 되면 id가 중복되어 저장되는 것처럼 보일 수 있으나 key값인 id를 통해서 Member(id, name) 객체를 얻을 수 있다는 점에 주목해야한다. 여기서는 회원 데이터가 id와 name 2개지만 예를들어 전화번호를 저장하는 phone이 추가되었다고 하면 (id, name)이렇게 저장하면 phone의 값은 가져올 수 없게 되는 문제가 생긴다.
그래서 member의 이름만 저장하는 것이 아니라 member 객체 인스턴스를 저장하는 것이다.
이렇게 하면 다음에 member 객체 인스턴스를 조회해서 필요한 데이터를 다 찾을 수 있습니다. 더 자세한 설명은 여기에서 확인할 수 있다.
Q. findById()에서 Optional.ofNullable(store.getId()) 로 리턴하는 이유는?
아래 코드 주석부분처럼 값을 바로 리턴하게 되면 그 값이 null일 경우에 NPE가 발생하게되는 문제가 생기므로 optional로 감싸서 반환해야한다.
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id)); // member가 null일 수도 있으니 optional로 감싸기
//return store.get(id); // null일 경우 NPE 발생하게되는 틀린 코드
}
- Optional.ofNullable(value)
null인지 아닌지 확신할 수 없는 객체를 담고 있는 Optional 객체를 생성합니다. Optional.empty()와 Optional.ofNullable(value)를 합쳐놓은 메소드라고 생각하시면 됩니다. null이 넘어올 경우, NPE를 던지지 않고 Optional.empty()와 동일하게 비어 있는 Optional 객체를 얻어옵니다. 해당 객체가 null인지 아닌지 자신이 없는 상황에서는 이 메소드를 사용하셔야 합니다
[참고] https://www.daleseo.com/java8-optional-after/
'Spring > 스프링 입문' 카테고리의 다른 글
[스프링 입문] 03. 회원관리 예제(백엔드 개발) - 회원 서비스 개발 (0) | 2022.04.27 |
---|---|
[스프링 입문] 03. 회원관리 예제(백엔드 개발) - 회원 리포지토리 테스트 케이스 작성 (0) | 2022.04.27 |
[스프링 입문] 03. 회원관리 예제(백엔드 개발) - 비즈니스 요구사항 정리 (0) | 2022.04.27 |
[스프링 입문] 02. 스프링 웹 개발 기초 - 정적 컨텐츠, MVC와 템플릿 엔진, API (0) | 2022.04.27 |
[스프링 입문] 01. 프로젝트 환경설정 - 빌드하고 실행하기 (0) | 2022.04.26 |