본문 바로가기

JAVA

[스프링 입문] 회원관리 예제 - 도메인, 레포지토리, 서비스

앞선 글을 통해 1️⃣화면을 띄우는 세가지 방법에 대해 알아봤다.
지금부터는 'MVC로 회원관리'라는 목표를 가지고 필요한 개념들을 학습할 것이다.
먼저, 이 글에서는 2️⃣도메인, 레포지토리, 서비스를 개발하고
다음 글에서는 MVC 모델을 구현하기 위해 필요한 개념인 3️⃣'의존성 주입'에 대해 학습하고,
그 다음 글에서는 4️⃣MVC로 회원관리를 해보고,
이를 5️⃣DB와 연결하며 JPA에 대해 배울 것이다.

[목차]

1. 비지니스 요구사항 정리
2. 클래스 의존 관계 설계
3. 회원 도메인과 레포지토리 만들기
4. 회원 레포지토리 테스트 케이스 작성
5. 서비스 개발
6. 서비스 테스트 케이스 작성

1. 비즈니스 요구사항 정리

어떤 비지니스 로직을 만들지 결정하기 전에, 무엇인지 요구되는지 정리할 필요가 있다.
우리 예제에서 관리할 요구사항은 다음과 같다.

데이터 : 회원ID, 이름
기능 : 회원 등록(save), 조회(findById, findByName, findAll)

 

2. 클래스 의존 관계 설계

일반적인 웹 어플리케이션 구조

  • 컨트롤러 : view와 비지니스 로직(서비스)의 매개체, model로 정보 전달
  • 서비스 : 핵심 비즈니스 로직 구현
  • 도메인 : 비즈니스 도메인 객체, 쉽게 말하면 관리할 대상 예) 회원, 주문, 쿠폰 등등
  • 레포지토리: 도메인 객체를 DB에 저장하고 관리하는 역할, DB에 접근하기 위해서는 레포지토리를 거쳐야 함

우리 예제의 어플리케이션 구조 (저장 공간 = 로컬 메모리)

  • 도메인으로는 회원 아이디와 이름이 담긴 Member 클래스를 사용한다.
  • 아직 데이터 저장소가 선정되지 않았기 때문에, 확장성을위해서 인터페이스로 레포지토리를 만든다.
    (추가되는 것은 이 인터페이스를 상속하게 하면 되므로)
    따라서 본 예제에서는 인터페이스인 MemoryRepository를 구현하는
    구현 클래스 MemoryMemberRepository를 레포지토리로 이용한다.

폴더 계층 구조

 

3. 회원 도메인과 레포지토리 만들기

회원 도메인 : Member 클래스

package hello.hellospring.domain;

public class Member { // 유저 id와 name & getter, setter 함수로 구성됨
	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;
	}
}

 

회원 레포지토리 인터페이스

package hello.hellospring.repository;

// Member 클래스가 아무리 public이라 하더라도, 다른 패키지에 있으므로 import 필요함
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);
	Optional<Member> findByName(String name);
	List<Member> findAll();
}
// 인터페이스 복습 : default 키워드를 생략해도 추상 메소드가 되며
// 이 인터페이스를 구현하는 모든 클래스는 추상 메소드를 강제적으로 구현해야 함

 

회원 레포지토리 구현체 - 위 인터페이스 상속 & HashMap에 도메인인 member 저장

package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.*;

public class MemoryMemberRepository implements MemberRepository {
	private static Map<Long, Member> store = new HashMap<>();
	private static long sequence = 0L; 

	@Override
	public Member save(Member member) {
		// static으로 선언 후 save에서 ++을 했으므로 
		// 저장된 순서에 따라 유니크한 아이디를 갖게 된다.
		member.setId(++sequence); 
		store.put(member.getId(), member); // put은 HashMap의 저장 함수 (key, value)
		return member;
	}

	@Override
	public Optional<Member> findById(Long id) {
		return Optional.ofNullable(store.get(id)); 
        // hashMap에서의 get(key)함수는 value즉, member 객체를 리턴함
		// null이 리턴되더라도 Optional.empty가 리턴됨
	}

	@Override
	public List<Member> findAll() {
		return new ArrayList<>(store.values());
	}

	@Override
	public Optional<Member> findByName(String name) {
		return store.values().stream()
		.filter(member -> member.getName().equals(name))
		.findAny(); // stream, filter, lambda는 나중에 정리
	}

	public void clearStore() {
		store.clear();
	}
}

 

4. 회원 레포지토리 테스트 케이스 작성

3번에서 작성한 회원 레포지토리가 제대로 작동되는지를 확인하고 싶다.
하지만 main에서 바로 돌리려면 전체를 다 돌려야 해서 비효율적이다.
이를 위해 자바는 JUnit이라는 프레임워크를 제공하여 기능별 test가 가능하게 한다.

본 예제에서 test 코드를 작성하는 순서는 다음과 같다.
① 테스트 코드를 올바른 위치에 생성하기
② 테스트에 필요한 api 임포트
③ 테스트하려는 클래스의 객체 만들기
@AfterEach로 한번의 테스트 이후 초기화해야 할 것 설정
⑤ 테스트 설계

① 테스트 코드를 올바른 위치에 생성하기

루트 폴더 / src / test / java

② 테스트에 필요한 api 임포트

import org.junit.jupiter.api.AfterEach; ➡️ @AfterEach
어떤 함수 앞에 이 어노테이션을 쓰면, 단일 테스트가 끝날 때마다 해당 함수가 실행된다.
주로 테스트끼리 영향받지 않도록 단일 테스트가 끝날 때마다 repository를 clear해주기 위해 사용한다.
테스트는 각각 독립적으로 실행되어야 한다.
테스트 순서에 의존 관계가 있는 것은 좋은 테스트가 아니다.

import org.junit.jupiter.api.Test; ➡️ @Test
특정 함수 앞에 이 어노테이션을 사용하면
main 함수를 실행하지 않고 해당 함수만 실행하는 것도 가능하다.
(방법 : 라인 옆에 있는 초록색 Run 버튼 클릭)

import static org.assertj.core.api.Assertions.*; ➡️  Assertions
테스트를 통해 기대하는 값과 실제 결과가 일치하는지 비교해주는 클래스
ex. assertThat(결과).isEqualTo(기대하는 값)
Assertions에서 지원하는 여러 메서드들이 특정 값을 리턴하거나 하진 않지만,
결과와 기대하는 값이 불일치하면 오류를 알려주듯이 테스트가 실패했다는 것을 알려준다.

3과 2+2가 다르므로 test가 실패했다.

③ 테스트하려는 클래스의 객체 만들기

ex. MemoryMemberRepository repository = new MemoryMemberRepository();
repository에서 만든 여러 메소드를 테스트하고 싶기 때문에 객체를 만들어줘야한다.

@AfterEach로 한번의 테스트 이후 초기화해야 할 것 설정

우리 예제에서는 레포지토리의 저장 상태를 초기화 해줘야 함

@AfterEach
public void afterEach() {
	repository.clearStore();
}

 

⑤ 테스트 설계

Assertion을 이용해서 예상값과 실제값을 비교하는 방법을 사용함

package hello.hellospring.repository; 
// test/java 폴더에 있으면서 main/java에 있는 repository와 같은 package인척 한다..

import hello.hellospring.domain.Member;
import java.util.List;
import java.util.Optional;
// 테스트에 필요한 api
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;

class MemoryMemberRepositoryTest {
	MemoryMemberRepository repository = new MemoryMemberRepository();
	
	@AfterEach
	public void afterEach() {
		repository.clearStore();
	}

	// 테스트하려는 함수가 잘 돌아가는지를 확인하는 함수
	// given(전제) - when(조건) - then(작동) 틀로 설계하면 좋다
	@Test
	public void save() {
		//given
		Member member = new Member();
		member.setName("spring");
		//when
		repository.save(member);
		//then
		Member result = repository.findById(member.getId()).get();
		// Optional 객체가 리턴되므로 .get()을 해줘야 Member 객체를 얻을 수 있다.
		assertThat(result).isEqualTo(member);
		// 로직 : 집어넣은 것을 저장해뒀다가, 꺼낸 값이랑 같은지 비교
	}

	@Test
	public void findByName() {
		//given
		Member member1 = new Member();
		member1.setName("spring1");
		repository.save(member1);
		Member member2 = new Member();
		member2.setName("spring2");
		repository.save(member2);
		//when
		Member result = repository.findByName("spring1").get();
		//then
		assertThat(result).isEqualTo(member1);
	}
		@Test
	public void findAll() {
		//given
		Member member1 = new Member();
		member1.setName("spring1");
		repository.save(member1);
		Member member2 = new Member();
		member2.setName("spring2");
		repository.save(member2);
		//when
		List<Member> result = repository.findAll();
		//then
		assertThat(result.size()).isEqualTo(2);
	}
}

 

5. 서비스 개발

package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import java.util.List;
import java.util.Optional;

public class MemberService {
    private final MemberRepository memberRepository;

    // 다형성을 위해 외부에서 받아옴 - DI(의존성 주입)
    public MemberService(MemberRepository memberRepository) { 
        this.memberRepository = memberRepository;
    }
    
    public Long join(Member member) {
    	// 중복 회원 검증 - 중복되면 예외 발생으로 프로그램 중단되므로
        // save되지 않는다.
        validateDuplicateMember(member); //중복 회원 검증
        memberRepository.save(member);
        return member.getId();
    }
    
    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
        .ifPresent(m -> {
        throw new IllegalStateException("이미 존재하는 회원입니다.");
        });
    }

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

 

6. 회원 테스트 케이스 작성

@BeforeEach 와 @AfterEach의 차이점 : 
BeforeEach는 주로 시작 환경 설정을, Afterach는 리소스 정리(종료)와 관련된 내용을 담당한다.

package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {
    MemberService memberService;
    MemoryMemberRepository memberRepository;
    
    @BeforeEach // 시작 환경 설정
    public void beforeEach() {
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository);
    }
    
    @AfterEach // 리소스 정리(종료)
    public void afterEach() {
        memberRepository.clearStore();
    }
    
    @Test
    public void 회원가입() throws Exception {
        //Given
        Member member = new Member();
        member.setName("hello");
        //When
        Long saveId = memberService.join(member);
        //Then
        Member findMember = memberRepository.findById(saveId).get();
        assertEquals(member.getName(), findMember.getName());
    }
    
    @Test
    public void 중복_회원_예외() throws Exception {
        //Given
        Member member1 = new Member();
        member1.setName("spring");
        Member member2 = new Member();
        member2.setName("spring");
        //When
        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class,
        () -> memberService.join(member2)); //예외가 발생해야 한다.
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }
}

 

이번 글에서는 최종적으로 만들어야 할 
컨트롤러 - 서비스 - 레포지토리 - 도메인 - DB 중에
노란색 부분을 만들었다.
다음 글에서는 본격적으로 컨트롤러를 구현하기 전에, 스프링 컨트롤러의 핵심 기능인 
의존성 주입에 대해 적을 예정이다.

'JAVA' 카테고리의 다른 글

[WIL] 5/08~5/14 컬렉션 프레임 워크  (0) 2023.05.14
[JAVA] 람다식, Stream, Optional  (0) 2023.05.11
스프링 빈과 자동 의존관계 설정  (0) 2023.04.03
[스프링 입문] 화면 띄우기  (1) 2023.03.07
BufferedReader, BufferedWriter  (0) 2022.10.25