본문 바로가기

WINK-(Web & App)/Spring Boot 스터디

[2024-2 SpringBoot 스터디] 윤성욱 #1주차

반응형

Spring 공부를 본격적으로 시작하면서, 김영한 개발자님의 <스프링 입문> 강의를 듣고 필요한 내용만 정리한 글이다.

비지니스 요구사항 정리

데이터 : 회원ID, 이름
기능 : 회원 등록, 조회
+ 데이터 저장소가 선정되지 않은 상태라고 가정 (RDB, NoSQL, ...)
→ 인터페이스를 사용하여 구현 클래스를 교체할 수 있도록 설계


Back-End

회원 객체 생성

domain Package > Member Class

 

user ID, name을 private로 생성 후 Getter and Setter로 접근 가능하도록 함

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 Package > MemberRepository Interface

 

save, findById, findByName, findAll 기능 추가
이때 findById, findByName 선언 시 Optional을 이용해 NPE(NullPointerException)가 발생하지 않도록 방지. null값이 반환될 수 있기에

public interface MemberRepository {

    Member save(Member member);
    Optional<Member> findById(Long id);
    Optional<Member> findByName(String name);
    List<Member> findAll();

}

 

회원 리포지토리 메모리 구현체 생성

repository Package > MemoryMemberRepository Class

 

MemberRepository Interface를 implements 해줌. 이후 option + enter로 implements method 진행

 

자, 이제 구현을 해야겠죠?

 

save의 경우, member의 name은 고객이 회원가입 시 적는 이름이고, id의 경우 시스템이 정해준다고 생각
findById의 경우, store에서 id 값을 꺼내오면 됨. null이 반환될 가능성이 있기에 Optional 이용
findByName의 경우, store에서 루프를 돌면서 파라미터로 넘어온 name과 같을 경우 필터링 되도록 함
findAll의 경우, 리스트를 이용해서 store에 있는 member가 반환 되도록 함

public class MemoryMemberRepository implements MemberRepository {

    // 동시성 문제가 고려되어 있지 않음, 실무에서는 ConcurrentHashMap, AtomicLong 사용 고려
    private static Map<Long, Member> store = new HashMap<>();
    // key 값을 자동으로 생성해주는
    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());
    }
}

 

구현을 했다면 동작하는지 확인을 해야겠죠?
이때는 테스트 케이스를 작성해보세요 ~

 

개발한 기능을 테스트할 때
main 메서드를 통해서 실행하거나, 웹 애플리케이션의 컨트롤러를 통해서 해당 기능을 실행한다
But, 이러한 방법은 준비하고 실행하는데 오래 걸리고, 반복 실행하기 어렵고 여러 테스트를 한번에 실행하기 어렵다는 단점이 있다
=> 자바는 JUnit이라는 프레임워크로 테스트 코드 자체를 실행해서 이러한 문제를 해결한다 (단위테스트)

 

회원 리포지토리 메모리 구현체 테스트

테스트 코드 작성 시 일반적인 관례. 테스트 하려는 코드명과 동일하게 작성하고 뒤에 test를 붙인다. ex. MemoryMemberRepositoryTest

 

테스트는 순서랑 상관없이 메소드별로 각각 독립적으로 실행되도록 설계해야 한다. 순서가 보장되지 않기에!

 

한번에 여러 테스트를 실행하면 메모리 DB에 직전 테스트의 결과가 남을 수 있다. 이전 테스트 때문에 다음 테스트가 실패할 가능성이 존재함
→ 이때, @AfterEach 를 작성하면 각 테스트가 종료 될 때 마다 메모리 DB에 저장된 데이터를 삭제한다

 

repository Package > MemoryMemberRepositoryTest Class

class MemoryMemberRepositoryTest {

    MemoryMemberRepository repository = new MemoryMemberRepository();

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

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

        Member result = repository.findById(member.getId()).get();
        assertThat(member).isEqualTo(result);
    }

    @Test
    public void findByName(){
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);

        Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2);

        Member result = repository.findByName("spring1").get();

        assertThat(result).isEqualTo(member1);

    }

    @Test
    public void findAll(){
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);

        Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2);

        List<Member> result = repository.findAll();

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

 

회원 서비스 개발

service Package > MemberService Class

 

서비스의 경우 naming에 있어 비즈니스에 가까운 용어를 사용해야 함

public class MemberService {

    private final MemberRepository memberRepository = new MemoryMemberRepository();

    /**
     * 회원가입
     */
    public Long join(Member member){
        // 같은 이름이 있는 중복 회원은 가입이 불가능하다고 가정.
        validateDuplicateMember(member); // 중복 회원 검증
        memberRepository.save(member);
        return member.getId();
    }

    /**
     * Optional<Member> result = memberRepository.findByName(member.getName())
     * result.ifPresent(m -> {
     *     throw new IllegalStateException("이미 존재하는 회원입니다.");
     * });
     *
     * Optional을 바로 반환하는 것은 별로 좋지 않기에 아래와 같은 방식을 권장함.
     * 또한, 위와 같은 경우에는 메소드로 뽑아냄.
     */
    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);
    }
}

 

private final MemberRepository memberRepository = new MemoryMemberRepository();

회원 리포지토리 메모리 구현체를 사용하기 위해 memberRepository 객체를 생성한 위의 코드를 아래와 같이 변형하면, MemberService 객체를 생성할 때 외부에서 선언된 memberRepository 객체를 주입받아 사용할 수 있게 된다.

→ 이와 같은 디자인 패턴을 의존성 주입이라고 부른다.

private final MemberRepository memberRepository;
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

 

서비스 테스트

given when then 문법을 사용해서 테스트 코드를 작성해보는 습관을 가져보자.

@BeforeEach를 작성하면 각 테스트 실행 전에 호출되어 테스트가 서로 영향이 없도록 항상 새로운 객체를 생성하고, 의존관계도 새로 맺어준다.

class MemberServiceTest {

    MemberService memberService;
    MemoryMemberRepository memberRepository;

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

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

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

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

        // then
        Member findMember = memberService.findOne(saveId).get();
        assertThat(member.getName()).isEqualTo(findMember.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 = assertThrows(IllegalStateException.class, () -> memberService.join(member2));

        /*
        // try-cathch를 사용해서 작성도 가능
        // when
        memberService.join(member1);
        try{
            memberService.join(member2);
            fail();
        } catch (IllegalStateException e) {
            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
        }
*/

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

 

반응형