본문 바로가기

springboot

[스프링 입문] 2. 스프링 웹 개발 기초 - 회원 관리 예제

  • 비즈니스 요구사항 정리
  • 회원 도메인과 레포지토리 만들기
  • 회원 레포지토리 테스트 코드
  • 회원 서비스 개발
  • 회원 서비스 테스트

 

비즈니스 요구사항 정리

데이터 : 회원 id, 이름

기능 : 회원 등록, 조회

* 아직 데이터베이스를 뭘 사용할 지 정하지 않은 상태라고 가정

일반적인 웹 애플리케이션 계층 구조

Controller : 웹 MVC의 Controller

Service : 핵심 비즈니스 로직 구현

Repository : DB에 접근, 도메인 객체를 DB에 저장하고 관리

Domain : 비즈니스 도메인 객체, ex) 회원, 주문, 쿠폰 등등 주로 데이터베이스에 저장하고 관리됨

 

 


 

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

DB가 선정되지 않았기 때문에 Repository를 Interface로 구현하고 변경할 수 있도록 설계하고, 첫번째로 구현체는 메모리 기반의 데이터 저장소를 사용한다.

 

클래스 의존관계

 

springpractice 패키지에 domain이라는 하위 패키지를 만들어 그 아래에 Member 객체를 만들었다.

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라는 패키지를 만들어 MemberRepository라는 인터페이스도 만들었다. 

MemberRepository가 해야할 일은 회원 데이터 등록, 조회가 있다. 

public interface MemberRepository {
    Member save(Member member);
    Optional<Member> findById(Long id);
    Optional<Member> findByName(String name);
    List<Member> findAll();
}

 

구현체인 MemoryMemberRepository도 만들어 기능을 구현했다. 

public class MemoryMemberRepository implements MemberRepository{

    private Map<Long, Member> store = new HashMap<>();
    private static Long sequence = 0L;

    @Override
    public Member save(Member member) {
        member.setId(++sequence);
        store.put(member.getId(), member);
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public Optional<Member> findByName(String name) {
        return store.values().stream()
                .filter(member -> member.getName().equals(name))
                .findAny();
    }

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

 


 

회원 레포지토리 테스트 코드

Repository의 기능이 제대로 동작하는지 확인하기 위해서 테스트케이스를 작성한다.

테스트 코드는 main 디렉토리가 아니라 test 디렉토리 하위에 작성한다.

 

테스트 코드는 given, when, then 단계로 나누어 작성하는 것이 좋다.

public class MemoryMemberRepositoryTest {

    MemoryMemberRepository repository = new MemoryMemberRepository();

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

    @Test
    public void save() {
        // given
        Member member = new Member();
        member.setName("spring");

        // when
        repository.save(member);

        // then
        Member result = repository.findById(member.getId()).get();
        Assertions.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(member1.getName()).get();

        // then
        Assertions.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
        Assertions.assertThat(result.size()).isEqualTo(2);
    }
}

 

Test는 코드 작성 순서대로 진행되지 않는다. 따라서 각 Test가 서로 독립적이기 위해서 store 데이터를 비워줘야한다. 

MemoryMemberRepository에 아래 코드를 추가한다.

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

@AfterEach를 이용해 한 테스트가 끝날 때마다 store를 비워주도록 한다. 

 

 

테스트를 돌리면 모두 성공하는 것을 확인할 수 있다.

회원 레포지토리 테스트 결과

 


 

회원 서비스 개발

도메인 객체를 관리하는 repository의 개발이 끝났으니, 실제 핵심 비즈니스 기능을 담당하는 서비스를 개발할 차례이다.

service 패키지와 하위에 MemberService 클래스를 새로 만들었다. 

Service는 Repository를 사용해서 로직을 구현한다. 

public class MemberService {
    
    private final MemberRepository memberRepository = new MemoryMemberRepository();
    
    // 회원 가입
    public Long join(Member member) {
        validateDuplicateMember(member); // 중복 회원 검증
        memberRepository.save(member);
        return member.getId();
    }
    
    public void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });
    }
    
    
    // 회원 목록 조회
    public List<Member> findMembers() {
        return memberRepository.findAll();
    }
    
    // 회원 id로 조회
    public Optional<Member> findOne(Long id) {
        return memberRepository.findById(id);
    }
}

 

간단한 기능이기 때문에 코드가 그리 길지 않았다. 

 


 

회원 서비스 테스트

test의 service 패키지를 만들어 Test 코드를 작성했다. 

Test 코드는 아래와 같다.

public class MemberServiceTest {

    private MemberService memberService = new MemberService();

    @Test
    public void 회원가입() {
        // given
        Member member = new Member();
        member.setName("spring");

        // when
        Long savedId = memberService.join(member);

        // then
        Member result = memberService.findOne(savedId).get();
        assertThat(member.getName()).isEqualTo(result.getName());
    }

    @Test
    public void 중복_회원_예외_처리() {
        // given
        Member member1 = new Member();
        member1.setName("spring");

        Member member2 = new Member();
        member2.setName("spring");

        // when
        memberService.join(member1);
        IllegalStateException e = Assertions.assertThrows(IllegalStateException.class,
                () -> memberService.join(member2));

        // then
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }

    @Test
    public void 회원_목록_조회() {
        // given
        Member member1 = new Member();
        member1.setName("spring1");
        memberService.join(member1);

        Member member2 = new Member();
        member2.setName("spring2");
        memberService.join(member2);

        // when
        List<Member> result = memberService.findMembers();

        // then
        assertThat(result.size()).isEqualTo(2);
    }
}

여기서도 test 데이터의 독립성을 위해서 clear를 해줘야하는데 MemberService 밖에 없기 때문에 clear를 해줄 수 없다. 

 

private MemberService memberService = new MemberService();
private MemoryMemberRepository repository = new MemoryMemberRepository();

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

repository와 afterEach의 clear하는 부분을 추가해준다. 

 

그렇다고 이런 식으로 추가하게 된다면 MemberService가 갖고있는 repository와 현재 repository가 다른 객체가 된다.

MemoryMemberRepository 객체를 두 개 사용할 이유가 없기 때문에 같은 객체를 사용하도록 수정한다. 

 

public class MemberService {

    private final MemoryMemberRepository memberRepository;

    public MemberService(MemoryMemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    
    ...
}

MemberService에서 MemoryMemberRepository를 생성자를 통해 받도록 한다.

 

public class MemberServiceTest {

    private MemoryMemberRepository repository;
    private MemberService memberService;

    @BeforeEach
    public void beforeEach() {
        repository = new MemoryMemberRepository();
        memberService = new MemberService(repository);
    }
    
    ...
}

Test에서는 각 Test 전에 repository와 service를 새로 만들어서 사용한다.

 

그렇게되면 결론적으로 하나의 repository를 사용할 수 있게 된다.