본문 바로가기

Spring/스프링 입문

02. 회원 관리 페이지 만들기 - (4) 회원 서비스 개발

💡 본 게시글은 김영한님의 인프런(Inflearn) 강의 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술에 대해 공부하고, 정리한 내용입니다.

1) MemberService 클래스 만들기

이번에는 회원 서비스(MemberService) 클래스를 만들어보겠습니다.

회원 서비스주로 회원 리포지토리와 도메인을 활용하여 비즈니스 로직을 작성하는 역할을 합니다.

 

① 먼저 'service' 패키지를 생성하고, 이 패키지 내에 'MemberService' 클래스를 생성해야 합니다.

'MemberService' 클래스는 회원 리포지토리를 활용하기 때문에 'MemoryMemberRepository' 객체를 선언해야 합니다. 이 'final' 키워드를 사용하여 선언하면, 이 객체는 한 번 할당된 후에는 변경할 수 없게 됩니다.

 

=> 이렇게 하면 안정성이 높아지며, 실수로 이 객체를 변경하는 것을 방지할 수 있습니다.

public class MemberService {
    // MemoryMemberRepository 객체 선언
    private final MemberRepository memberRepository = new MemoryMemberRepository();
}

2) 회원 가입 기능 (Join) 만들기

이제 회원 가입 기능을 구현하겠습니다. 회원 가입 기능은 Member 객체를 Repository에 저장하는 기능이라고 볼 수 있습니다.

 

 따라서 memberRepository의 save 메서드를 호출하면 됩니다. 그리고 회원 가입이 성공하면, 저장된 멤버의 id를 반환하도록 합니다.

public long join(Member member){
    memberRepository.save(member);
    return member.getId();
}

 

 그런데 여기에 추가적인 비즈니스 요구사항이 있습니다. 바로 "같은 이름이 있는 회원은 가입이 불가능하다"는 것입니다. 이를 처리하기 위해 다음과 같이 코드를 수정하겠습니다.

/**
* 회원 가입
*/

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 메서드 호출

  • 먼저, join 메서드가 호출되며, parameter로 Member 객체가 전달됩니다. 이 Member 객체는 사용자로부터 입력받은 회원 정보를 담고 있습니다.
public long join(Member member) { ... }

 

② 중복 회원 검증

  • join 메서드 내에서 첫 번째로 호출되는 것은 validateDuplicateMember 메서드입니다. 이 메서드는 중복 회원을 검증하는 역할을 합니다.
validateDuplicateMember(member); // 중복 회원 검증

 

③ validateDuplicateMember 메서드 동작

  • validateDuplicateMember 메서드 내에서는 memberRepository의 findByName 메서드를 호출하여, 같은 이름을 가진 회원이 이미 존재하는지 검사합니다.
  • findByName 메서드는 Optional 객체를 반환하므로, ifPresent 메서드를 이용해 Optional 객체 안에 값이 있다면 (즉, 같은 이름의 회원이 이미 존재한다면) IllegalStateException을 발생시킵니다.
private void validateDuplicateMember(Member member) {
    memberRepository.findByName(member.getName())
        .ifPresent(m -> {
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        });
}
  • validateDuplicateMember 메서드에서 예외가 발생하지 않았다면, 중복 회원이 없다는 뜻이므로 memberRepository의 save 메서드를 이용하여 회원 정보를 저장합니다.
memberRepository.save(member);

 

⑥ 회원 ID 반환

 

  • 마지막으로, 저장된 회원의 ID를 반환합니다. 이 ID는 회원을 유일하게 식별하는 값입니다.
return member.getId();

 

 이렇게 하면, "같은 이름이 있는 회원은 가입이 불가능하다"는 비즈니스 요구사항을 충족시킬 수 있습니다. 더불어, 'validateDuplicateMember' 메서드를 별도로 분리함으로써 'join' 메서드의 코드를 간결하게 유지하고, 각 메서드가 하나의 기능만 수행하도록 함으로써 코드의 가독성유지보수성을 향상시킬 수 있습니다.


3) Java의 Optional 간단히 이해하기

 Java 8 이전에는 객체가 null일 가능성이 있을 때, if(object != null)과 같은 조건문을 사용하여 검사했습니다. 이 방식의 문제는 코드가 길어지고 가독성이 떨어진다는 점입니다. Optional을 사용하면 이런 단점을 극복할 수 있습니다. 

 

(1) Optional<T>란?

 Optional<T>T 타입의 객체를 담는 Wrapper 클래스입니다. 그래서 Optional 인스턴스는 모든 타입의 참조 변수를 저장할 수 있습니다. 더욱이 NullPointerException을 Optional에서 제공하는 메서드를 통해 처리할 수 있습니다.

 

(2) Optional 객체 생성?

  Optional 객체를 생성하는 방법은 of()ofNullable()를 이용하는 것입니다. null이 될 가능성이 있는 경우에는 ofNullable()을 사용하여 null을 처리하면 됩니다.

 

(3) Optional 객체 내부의 값 접근

그럼 Optional 객체 내부의 값에는 어떻게 접근하나요?

 

 get()을 사용하면 접근이 가능합니다. 하지만 Optional 객체 내부에 null이 있을 수 있으므로 값이 null인지 확인하는 작업이 필요합니다.

 

 isPresent()를 사용하면 해당 값이 null인지 확인할 수 있지만, boolean 값을 반환하므로 결국 조건문을 써야하는 문제가 있습니다. 이를 해결하기 위해 ifPresent()가 사용되며, 이 메서드는 Optional 객체 내의 값이 null이 아닐 때만 뒤의 로직(람다식)을 실행하게 합니다. 이로써 코드의 가독성이 더욱 향상됩니다.

 

(3) Optional  메소드

 아래는 자주 사용되는 Optional 메서드들을 정리한 표입니다. 더 자세한 내용은 링크를 참조하세요.

메소드 명 설명
get() Optional 객체에 저장된 값을 반환합니다.
isPresent() Optional 객체에 저장된 값이 있는지 확인하고 boolean형으로 반환합니다.
ifPresent() Optional 객체에 저장된 값이 있다면 이하의 람다식을 실행합니다.
orElse(T other) Optional 객체에 저장된 값이 있다면 이하의 람다식을 실행합니다.
orElseGet() 저장된 값이 있다면 그 값을 반환하고, 없다면 인수로 전달된 람다식의 결과 값을 반환합니다.
orElseThrow() 저장된 값이 있다면 그 값을 반환하고, 없다면 인수로 전달된 예외를 발생시킵니다.

4) 전체 회원 조회하는 기능 (findMembers) 만들기

 이전에 MemoryMemberRepository에서는 모든 회원 정보를 반환하는 findAll 메서드를 구현했습니다. 이 메서드는 모든 회원 객체를 List에 담아 반환합니다.

 

 이제 이를 활용하여 모든 회원을 조회하는 findMembers 메서드를 만들어 보겠습니다. 이 메서드는 MemoryMemberRepository의 findAll() 메서드의 결과를 그대로 반환하면 됩니다.

// 전체 회원 조회
public List<Member> findMembers(){
    return memberRepository.findAll();
}

이렇게 하면 findMembers 메서드를 호출하면 전체 회원 정보 리스트가 반환됩니다.


5) 회원 한 명 조회하는 기능 (findOne) 만들기

 모든 회원을 조회하는 기능을 구현한 후에는, 특정 회원 한 명을 조회하는 기능도 만들어봅시다.

 

회원의 Id를 파라미터로 받아, 해당 Id를 갖고 있는 회원 객체를 반환하는 메서드를 구현하면 됩니다. MemoryMemberRepository의 findById() 메서드를 이용하면 특정 Id를 갖는 회원을 찾을 수 있습니다.

public Optional<Member> findOne(Long memberId){
    return memberRepository.findById(memberId);
}

이렇게 하면 findOne 메서드를 호출하면서 특정 회원의 Id를 전달하면, 그 회원의 정보가 담긴 Optional<Member> 객체가 반환됩니다. 이 Optional 객체를 이용해서 회원 정보를 안전하게 처리할 수 있습니다.


6) 클래스의 역할에 따른 용어 사용

 우리는 지금까지 회원 리포지토리 클래스와 회원 서비스 클래스를 만들어왔습니다. 이 두 클래스가 각각 어떤 역할을 하는지, 그리고 이에 따라 어떤 용어를 사용하는지 살펴보겠습니다.

 

🔎 회원 리포지토리 클래스

 회원 리포지토리 클래스는 회원 데이터를 관리하는 역할을 합니다. 따라서 이 클래스에서는 데이터를 다루는 작업에 직관적인 이름을 사용합니다. 예를 들어, save, findByName, findById, findAll과 같은 메서드 이름을 사용합니다. 이런 이름은 '저장소에 넣다', '빼다'와 같은 간단한 동작을 나타냅니다.

 

🔎 회원 서비클래스

 회원 서비스 클래스는 비즈니스 로직을 담당합니다. 따라서 이 클래스에서는 비즈니스 용어에 가까운 이름을 사용합니다. 예를 들어, join, findMembers와 같은 메서드 이름을 사용합니다. 이런 이름은 비즈니스 프로세스를 반영하며, 기획자나 다른 비즈니스 관계자들이 이해하기 쉽습니다. 예를 들어, "회원 가입 쪽이 이상합니다"라는 피드백이 있을 때, join 메서드를 바로 살펴볼 수 있습니다.

 

💡 결론

 각 클래스의 역할에 따라 메서드명을 작성해야 합니다. 이렇게 하면 코드의 가독성이 높아지고, 비즈니스 로직을 이해하고 수정하는 데 도움이 됩니다.