Spring

[Spring] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 #3

solfa 2024. 9. 6. 04:33

본 글은 해당 강의를 보고 작성한 글입니다.

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8

 

[지금 무료] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 강의 | 김영한 - 인프

김영한 | 스프링 입문자가 예제를 만들어가면서 스프링 웹 애플리케이션 개발 전반을 빠르게 학습할 수 있습니다., 스프링 학습 첫 길잡이! 개발 공부의 길을 잃지 않도록 도와드립니다. 📣 확

www.inflearn.com


#3 회원 관리 예제 - 백엔드 개발

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

- 데이터 : 회원 ID,이름

- 기능 : 회원 등록, 조회

- 데이터 저장소(DB)가 선정 안 된 상태에서 개발하는 상황이라고 가정한다.

- 컨트롤러: 웹 MVC의 컨트롤러 역할, API 만들기 등

- 서비스 : 핵심 비즈니스 로직 구현(ex. 같은 이름 회원 중복 안 됨), 비즈니스 도메인 객체를 가지고 핵심 비즈니스 로직이 동작하도록 구현한 계층

- 리포지토리 : 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리

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

 

<클래스 의존관계>

-회원리포지토리(회원 저장)는 인터페이스로 설계

  -> 왜냐하면 아직 데이터 저장소가 선정되지 않았기에 구현 클래스를 변경할 수 있도록 하기 위함

-초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 자장소 사용

  -> 추후에 구체적인 기술(RDB, NoSQL 등)이 선정되면 바꿔끼우는 과정을 거침


2. 회원 도메인과 회원 도메인 객체를 저장하고 불러올 수 있는 저장소인 리포지토리 만들기

src/main/java/hello.hellospring/domain 패키지 생성

domain/Member 클래스 생성

package hello.hellospring.domain;

public class Member {
    private Long id; //시스템이 지정하는 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;
    }
}

 

src/main/java/hello.hellospring/repository 패키지 생성 (회원 객체를 저장할 저장소를 만드는 것!)

repository/MemberRepository 인터페이스 생성

package hello.hellospring.repository;

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); // ID로 회원 조회, null 반환시 optional로 감싸서
    Optional<Member> findByName(String name); // 이름으로 회원 조회
    List<Member> findAll(); // 저장된 전체 회원 조회
}

 

repository/MemoryMemberRepository 클래스 생성

-implements MemberRepository 

-ctrl + space 또는 option + enter : import(implements)

package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.*;
/**
* 동시성 문제가 고려되어 있지 않음, 실무에서는 ConcurrentHashMap, AtomicLong 사용 고려 */
public class MemoryMemberRepository implements MemberRepository {
    private static 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 List<Member> findAll() {
        return new ArrayList<>(store.values());
    }
}

- private static Map<Long, Member> store = new HashMap<>() : (key: Long(id), value: Member)

- sequence : 0, 1, 2 ... key 값 생성

- save() : store에 넣기 전에 Member의 id값을 세팅(name은 넘어온 상태)하고 store에 저장

- findById() : store에서 id를 통해 get하면 됨. 그런데 결과가 없으면 null이 반환될텐데, Optional.ofNullable로 감싸서 반환해주면 클라이언트에서 무언가 할 수 있음

 - findByName() : member의 name이 파라미터로 넘어온 name과 같은지 확인하여 같으면 반환, 끝까지 찾았는데 없으면 Optional에 null이 포함돼서 반환됨

- findAll() : 실무에선 루프돌리기 편함 등의 이유로 List 많이 사용

동작을 검증하는 방법은? 테스트 케이스 작성으로!

3. 회원 리포지토리가 정상적으로 동작하는지 테스트 케이스 작성

- 개발한 기능이 제대로 작동하는지 검증하기 위해 테스트 케이스를 작성함

- 자바의 main 메서드를 통해 실행하거나 웹 애플리케이션의 컨트롤러로 검증하는 것은 반복 실행이 어렵고 여러 테스트를 한번에 실행하기 어렵다.

- 따라서 자바에서는 JUnit이라는 프레임워크로 테스트 코드를 만들어서 테스트 코드 자체를 실행하여 이런 문제를 해결함

 

src/test/java/repository 패키지 생성

src/test/java/repository/MemoryMemberRepositoryTest 클래스 생성

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

- public일 필요는 없다

 

save 테스트

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

        repository.save(member);
        Member result = repository.findById(member.getId()).get();
        //System.out.println("result = Assertions.assertEquals(member,result); // 1번
        //Assertions.assertEquals(result, member);// 2번, jupiter - member와 result가 다른 값이면 오류 발생
        //assertThat(member).isEqualTo(result); // 3번, assertj - 다르면 오류

    }

- command + shift + enter 잘 사용하자! 개꿀인듯 언제 다 외워 근데

- findById(member.getId()).get() : findById의 반환 타입이 Optional인데, get()으로 꺼낼 수 있음 (좋은 방법은 아님)

- 검증 1번 : 같으면 true 출력

- 검증 2번 : junit의 Assertions의 assertEquals - 같으면 정상 실행, 다르면 오류

- 검증 3번 : assertj의 Assertions의 assertThat - 같으면 정상 실행, 다르면 오류

 

findByName 테스트

@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); //다른 객체라면 오류발생
}

 

테스트 케이스의 장점 : 동시에 여러 클래스를 테스트할 수 있음!

 

findAll 테스트

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

- 사이즈가 같으면 정상 실행이고 다르면 오류임

- 그런데 이후에 전체를 테스트해보면 에러 발생함 -> 순서 의존적이기 때문에 테스트 순서가 보장이 안된다.

- findAll()이 먼저 실행되어서 findByName()할 때 이전에 저장된 것 때문에 에러가 발생

-> 따라서 테스트 한번 끝나면 데이터 clear 해주는 과정 필요!

 

src/test/java/repository/MemoryMemberRepositoryTest 클래스에 아래 코드 추가

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

- @AfterEach : 메서드 실행 끝날때마다 동작

 

repository/MemoryMemberRepository 클래스에 아래 코드 추가

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

- 저장소 지움

 

테스트는 서로 순서 의존관계없이 설계되어야 함! -> 테스트 하나 끝날때마다 저장소, 공용 데이터 깔끔하게 지워줘야 함

 

정리하자면

- 개발 먼저하고 테스트 케이스 작성함.

- 테스트 케이스 작성후 개발 : TDD(테스트 주도 개발)

- 테스트 코드는 필수

- 테스트 코드가 여러개면 클래스나 패키지 단위로 실행해도 됨(gradlew test 등)


4. 실제 비즈니스 로직이 있는 회원 서비스 개발

 

회원 서비스 : 회원 리포지토리와 도메인을 활용하여 실제 비즈니스 로직을 작성

hello.hellospring/service 패키지 생성

hello.hellospring/service/MemberService 클래스 생성

package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import java.util.List;
import java.util.Optional;
public class MemberService {
    private final MemberRepository memberRepository = new MemoryMemberRepository();
    /**
     * 회원가입 */
    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("이미 존재하는 회원입니다.");
                }); }
    /**
     *전체 회원 조회 */
    public List<Member> findMembers() {
        return memberRepository.findAll();
    }
    public Optional<Member> findOne(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

※ command + option + v

※ ctrl + t + Extract Method : 메서드로 추출

※ 서비스 클래스는 비즈니스에서 가져온 이름 써야 함(쉬운 매칭을 위해) -> 서비스는 비즈니스에 의존적으로 설계

※ 리포지토리는 서비스보단 단순히 개발스럽게 네이밍


5. 회원 서비스가 정상 동작하는지 제이유닛이라는 테스트 프레임워크를 만들어서 테스트

 

command + shift + t : 테스트 파일 생성

테스트의 경우 과감하게 한글로 해도 됨 + 빌드될때 테스트 코드는 포함되지 않음

 

given-when-then 문법

: test는 이런 상황(given)에서 이걸 실행햇을때(when) 이런 결과(then)가 나와야 한다.

: given - 이 데이터를 기반으로 when - 검증하는 것 then - 검증

void join() {
        //given
        Member member = new Member();
        member.setName("hello");
        //when
        Long saveId = memberService.join(member);

        //then
        Member findMember = memberService.findOne(saveId).get();
        Assertions.assertThat(member.getName()).isEqualTo(findMember.getName());
    }
@Test
    public void duplicate(){
        //given
        Member member1 = new Member();
        member1.setName("spring");

        Member member2 = new Member();
        member2.setName("spring");
        //when
        memberService.join(member1);
        try{
            memberService.join(member2);
            fail(); //
        }
        catch(IllegalStateException e){
            Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
        }
        //then
    }

- try-catch 사용하는 방법 : 애매한 방법... 별로 안쓰이는 것 같다!

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

        Member member2 = new Member();
        member2.setName("spring");
        //when
        memberService.join(member1);
        /*1번*/
        //assertThrows(IllegalStateException.class, () -> memberService.join(member2));
        
        /*2번*/
        /*command + option + v*/
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
        //then
    }

- 1번 : memberService.join(member2) <- 이 로직 실행 시, IllegalStateException <- 이 예외 발생해야 한다!  

- 2번 : e로 받아서 확인! 

- join()에서 spring으로 setName을 변경하고 전체 테스트를 진행하면, 위와 같이 실패한다.

이유는 위에서 봤듯이 메모리 저장소에 값이 누적되기 때문이다

따라서 clear를 해줘야 함

MemoryMemberRepository memberRepository = new MemoryMemberRepository();

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

- 실행될 때마다 메모리가 clear 해주기

- MemberServiceTest와 MemberService에서 각각 MemoryMemberRepository 객체를 생성하고 있다. 즉, 이 두 객체는 서로 다른 객체이다.

- 현재는 MemoryMemberRepository.java내에 store가 static으로 선언되어 서로 다른 객체더라도 같은 db이지만, static이 아니라면 다른 db가 되면서 문제가 발생한다. 

- 따라서 같은 인스턴스를 쓰게 하기 위해 아래와 같이 해줘야 한다.

- MemberService에서 memberRepository를 직접 내부에서 생성하는 것이 아니라, 외부에서 넣어주도록 하는 것이다. 

-  @BeforeEach : 각 테스트 실행 전에 호출됨

- MemoryMemberRepository 객체를 생성하여 memberRepository에 넣어주고, 그걸 memberService에도 넣어줌으로써 같은 MemoryMemberRepository를 사용하게 됨

- MemberService 입장에서 memberRepository를 내부에서 직접 만드는 것이 아니라 외부에서 넣어주고 있다 --> 이런 걸 DI라 한다!!

- DI : Dependency Injection

 

package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import java.util.List;
import java.util.Optional;
public class MemberService {
    private final MemberRepository memberRepository;

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

    /**
     * 회원가입 */
    public Long join(Member member) {
        validateDuplicateMember(member); //중복 회원 검증 memberRepository.save(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);
    }
}
package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
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
    void join() {
        //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 duplicate(){
        //given
        Member member1 = new Member();
        member1.setName("spring");

        Member member2 = new Member();
        member2.setName("spring");
        //when
        memberService.join(member1);
        /*command + option + v*/
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
        //then
    }
    @Test

    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

 

728x90