이 포스트는 김영한님의 ‘스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술’을 수강하고 작성하였습니다.

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

컨트롤러서비스리포지토리도메인DB출처: 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 (김영한)
  • 컨트롤러: 웹 MVC의 컨트롤러 역할
  • 서비스: 도메인을 이용하여 핵심 비즈니스 로직 구현
  • 리포지토리: 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리
  • 도메인: 비즈니스 도메인 객체, DB에 저장하고 관리
    • 예) 회원, 쿠폰, 주문 등등

클래스 의존관계

MemgerServiceinterfaceMemberRepositoryMemoryMemberRepository출처: 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 (김영한)
  • 인터페이스로 구현 클래스를 변경할 수 있도록 설계
  • 가벼운 개발을 위해 초기 개발 단계에서는 가벼운 메모리 기반의 데이터 저장소 사용

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

깃허브 리포지토리 주소: https://github.com/nyj001012/springboot_tutorial

도메인 만들기

package hello.hellospring.domain;

import java.util.concurrent.atomic.AtomicLong;

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;
    }
}

리포지토리 만들기

리포지토리 인터페이스

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;

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

메모리 리포지토리

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

public class MemoryMemberRepository implements MemberRepository {
    private static ConcurrentHashMap<Long, Member> store = new ConcurrentHashMap<>();
    private static AtomicLong sequence = new AtomicLong(0);

    @Override
    public Member save(Member member) {
        sequence.incrementAndGet();
        member.setId(sequence.longValue());
        store.put(sequence.longValue(), 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<Member>(store.values());
    }

    public void clearStore() {
        store.clear();
        sequence = new AtomicLong(0);
    }
}

원래 강의에서는 AtomicLong 대신 Long을 사용하였는데, 강의에서 “실무에서는 동시성 문제 때문에 AtomicLong을 사용하지만, 여기서는 간단하게 Long을 사용하겠다.”는 말을 듣고 궁금해서 사용했다. 이 부분에 대해서는 따로 포스트를 작성하도록 하겠다.

AtomicLong과 마찬가지로 ConcurrentHashMap 또한 같은 이유에서 사용했다.

테스트 코드 작성

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.junit.jupiter.api.*;

import java.util.List;
import java.util.Optional;

import static org.assertj.core.api.Assertions.*;

public class MemoryMemberRepositoryTest {
    MemoryMemberRepository repository = new MemoryMemberRepository();

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

    @Nested
    class TestSave {
        @Test
        void save() {
            Member yejin = new Member();
            yejin.setName("예진");
            Member saveResult = repository.save(yejin);
            assertThat(saveResult).isEqualTo(yejin);
            assertThat(saveResult.getId().longValue()).isEqualTo(1);
        }
    }

    @Nested
    class TestExcludeSave {
        @BeforeEach
        void beforeEach() {
            Member yejin = new Member();
            Member yena = new Member();
            yejin.setName("예진");
            yena.setName("예나");
            repository.save(yejin);
            repository.save(yena);
        }

        @Test
        void findById() {
            Optional<Member> findResult = repository.findById(1L);
            Member member = findResult.orElse(null);
            assertThat(member).isNotNull();
            assertThat(member.getName()).isEqualTo("예진");
        }

        @Test
        void findByIdFail() {
            Optional<Member> findResult = repository.findById(-2L);
            assertThat(findResult.isEmpty()).isTrue();
        }

        @Test
        void findByName() {
            Optional<Member> findResult = repository.findByName("예진");
            Member member = findResult.orElse(null);
            assertThat(member).isNotNull();
            assertThat(member.getId()).isEqualTo(1);
        }

        @Test
        void findByNameFail() {
            Optional<Member> findResult = repository.findByName("고구마");
            assertThat(findResult.isEmpty()).isTrue();
        }

        @Test
        void findAll() {
            List<Member> memberList = repository.findAll();
            assertThat(memberList.size()).isEqualTo(2);
        }
    }
}

테스트 코드도 강의보다는 조금 더 자세하게 작성했다. @BeforeEach로 각 테스트 케이스가 시작되기 전에, repository에 데이터를 저장하는 일을 했다.

save()함수 테스트 케이스 동작 시 기존 repository에 데이터를 저장하는 작업이 필요 없어서, @Nested 어노테이션을 이용하여 미리 repository에 데이터를 저장해야하는 케이스들과 아닌 케이스들로 나누었다.

@AfterEach로는 repository를 초기화하는 역할을 하기 때문에, 모든 @Nested 클래스에게 똑같이 적용되도록 중첩 클래스 바깥에 정의해두었다.

댓글남기기