Spring 입문 Chapter 3-4. 회원 관리 예제 : 회원 서비스 개발
이 글은 인프런에 있는 김영한님의 "스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술" 강의를 듣고 정리한 필기입니다.
⛅️ MemberService 클래스 만들기
이번 시간에는 회원 서비스(MemberService) 클래스를 만들어보자.
회원 서비스는 회원 리포지트리와 도메인을 활용해서 비지니스 로직을 작성하는 것입니다.
먼저 java package에 service package를 만든다. 그리고 service 패키지에 MemberService 클래스를 생성한다.
회원 서비스 자체가 회원 리포지트리를 활용하는 것이기 때문에 MemberService 클래스에 MemoryMemberRepository 객체를 선언한다.
public class MemberService {
// MemoryMemberRepository 객체 선언
private final MemberRepository memberRepository = new MemoryMemberRepository();
}
⛅️ 회원 가입 기능(Join) 만들기
회원 가입은 Member 객체를 Repository에 저장하는 기능이라고 볼 수 있다. 고로 memberRepository의 save를 호출하면 될 것이다.
회원 가입을 하면 repository에 멤버 정보를 저장하고, id를 반환한다고 정의하자.
/**
* 회원 가입
*/
public long join(Member member){
memberRepository.save(member);
return member.getId();
}
그런데 이전에 비지니스 요구사항을 정리할 때 "같은 이름이 있는 회원은 가입이 안된다"라는 항목이 있었다.
이 항목은 어떻게 해야 할까?
1. parameter로 받은 member 객체의 이름(name)을 가진 객체를 Optional로 받는다.
2-1. Optional 안에 들어있는 값이 null이 아니라면, 해당 이름을 가진 객체가 있다는 뜻이다.
따라서 같은 이름이 있는 회원이 있다고 알리고, 가입을 거부해야 한다.
2-2. 만일 Optional 안에 들어있는 값이 null이라면, 해당 이름을 가진 객체가 없다는 뜻이다.
따라서 회원 가입을 진행해야 한다.
위와 같은 흐름을 따라가면 될 것이다. 흐름에 맞게 코드를 짜보자.
// 같은 이름이 있는 중복 회원 X
Optional<Member> result = memberRepository.findByName(member.getName());
result.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
memberRepository.save(member);
먼저 memberRepository에서 findByName을 통해 member의 이름(member.getName())에 해당하는 객체를 받아서 변수 result에 담는다.
result는 Optional 객체이기 때문에, Optional에서 제공하는 메서드를 사용할 수 있다. 그 중 ifPresent()를 사용해보자.
ifPresent(Consumer<? super T> consumer)는 Optional 객체에 값이 있다면(null이 아니라면), 뒤의 로직을 실행하는 함수이다.
따라서 result에 같은 이름인 회원이 존재한다면, throw new IllegalStateException을 던질 것이다.
위의 코드를 밑처럼 개선할 수 있다.
// 같은 이름이 있는 중복 회원 X
memberRepository.findByName(member.getName())
.ifPresent(m ->{
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
findByName으로 받은 값에 바로 ifPresent를 붙여서 사용하는 것이다. findByName의 return 값은 Optional<Member>이기 때문에 바로 Optional<Member>.ifPresent를 사용할 수 있다.
그런데 잘 보면, memberRepository~ 부분이 따로 로직이 있는 부분임을 알 수 있다.
메서드에는 하나의 행동만 들어있는 것이 더 좋기 때문에, 위 부분을 메서드로 따로 만들도록 하자.
메서드로 뽑고 싶은 부분을 드래그하고 Alt+Enter 선택 - Extract Method 선택 - 메서드 이름 지정하면 된다.
회원 가입 메서드 join의 최종 결과는 이렇다.
/**
* 회원 가입
*/
public long join(Member member){
validateDuplicateMember(member); // 중복 회원 검증
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
이렇게 하면 join 메서드의 코드만 보고도 회원 가입이 어떤 흐름으로 이루어지는지 쉽게 해석할 수 있다.
⛅️ 간단한 Optional 정리
java 8 이전에는 객체의 값이 null이 가능성이 있을 때, if(object != null)과 같이 조건문을 사용해서 검사하는 방식을 사용했다.
하지만 위와 같은 방식은 코드가 길어지고 가독성이 떨어진다는 단점이 있다.
Optional을 사용하면 이런 단점을 보완할 수 있다.
Optional<T>는 T type의 객체를 감싸는 Wrapper class이다. 따라서 Optional 인스턴스는 모든 type의 참조 변수를 저장할 수 있다.
또한 NullPointerException을 Optional에서 제공하는 메서드로 해결 할 수 있다.
of()나 ofNullable()을 이용해 Optional 객체를 생성할 수 있다. null이 될 가능성이 있는 경우 ofNullable()로 생성하면 null을 처리할 수 있다.
그렇다면 Optional 객체 안에 있는 값에는 어떻게 접근할까?
.get()으로 접근이 가능하지만, Optional 객체 안에 null이 있을 수 있으므로 값이 null인지의 여부를 확인해야 한다.
.isPresent()를 사용하면 해당 값이 null인지의 여부를 확인할 수 있지만, boolean 값을 반환하기 때문에 결국엔 조건문을 써야한다는 불편함이 있다.
이를 해결한 것이 .ifPresent()인데, .ifPresent()를 사용하면 Optional 객체 안의 값이 null이 아닐 때에만 뒤의 로직(람다식)을 실행한다.
이렇게하면 코드의 가독성이 더 좋아진다.
많이 사용되는 Optional 메서드들을 모아봤다.
더 자세한 내용은 밑 링크를 참고하자.
https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html
메서드 명 | 설명 |
get() | Optional 객체에 저장된 값을 반환한다. |
isPresent() | Optional 객체에 저장된 값이 있는지 확인하고 boolean형으로 반환한다. |
ifPresent() | Optional 객체에 저장된 값이 있다면 이하의 람다식을 실행한다. |
orElse(T other) | Optional 객체에 저장된 값이 있다면 이하의 람다식을 실행한다. |
orElseGet() | 저장된 값이 있다면 그 값을 반환하고, 존재하지 않으면 인수로 전달된 Lambda 식의 결과 값을 반환한다. |
orElseThrow() | 저장된 값이 있다면 그 값을 반환하고, 존재하지 않으면 인수로 전달된 예외를 발생시킨다. |
⛅️ 전체 회원 조회하는 기능(findMembers) 만들기
우리는 이전에 MemoryMemberRepository에서 모든 회원 정보를 받는 메서드 findAll을 만들었다.
findAll은 모든 회원 객체를 List에 담아 반환한다.
따라서 전체 회원 조회하는 메서드인 findMembers에서는 MemoryMemberRepository의 findAll()의 결과를 반환해주면 될 것이다.
// 전체 회원 조회
public List<Member> findMembers(){
return memberRepository.findAll();
}
⛅️ 회원 한 명 조회하는 기능(findOne) 만들기
모든 회원들을 조회하는 기능을 만들었으니, 회원 한 명을 조회하는 기능도 만들어보자.
멤버의 Id를 전달 받으면, 해당 Id를 갖고 있는 객체를 받아서 반환하면 될 것이다.
public Optional<Member> findOne(Long memberId){
return memberRepository.findById(memberId);
}
⛅️ 클래스의 역할에 따라 용어를 다르게 사용하자.
우리는 지금까지 회원 리포지트리 클래스와 회원 서비스 클래스를 만들어보았다.
먼저 회원 리포지트리 클래스의 인터페이스인 MemberRepository 인터페이스의 메서드명을 살펴보자.
save, findByName, findById, findAll. 이와 같이 단순히 저장소에 넣었다 뺐다하는 이름을 사용하고 있다.
다음으로 회원 서비스 클래스 MemberService 클래스의 메서드명을 살펴보자.
join, findMembers과 같이 비지니스에 좀 더 가까운 느낌을 받을 수 있다.
보통 서비스 클래스는 비지니스에서 가지고 온 용어를 사용해야 한다. 만일 기획자가 "회원 가입 쪽이 이상합니다"라고 이야기를 했을 때, join 메서드를 바로 살펴볼 수 있어야 하기 때문이다.
즉, matching이 되어야 한다.
각 클래스의 role에 맞도록 메서드명을 작성해야 한다.