[Spring] 인프런 강의 필기 (2) - 회원관리 예제 (백엔드 개발)

김재연·2022년 10월 13일
0

Spring Boot 공부

목록 보기
5/9
post-thumbnail

1. 스프링 동작 계층 구조

컨트롤러 : 웹 MVC의 컨트롤러 역할
서비스 : 핵심 비즈니스 로직 구현
리포지토리 : 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리
도메인 : 비즈니스 도메인 객체 (ex. 회원, 주문, 쿠폰 등등 주로 데이터베이스에 저장하고 관리됨)


2. 회원 도메인 만들기

src/main/java/hello/hellospringdomain 패키지(폴더)를 만들고, 그 안에 Member.java 파일을 만든다.

.java 파일을 만들면 자동으로

이런게 뜬다. 용도에 따라 선택하면 되는데, 지금은 클래스를 만들거니까 class로 선택하고 사용할 필드들을 쓴다.

// src/main/java/hello/hellospring/Member.java
package hello.hellospring.domain;

public class Member {
    private Long id;
    private String name;
}

클래스 안에 커서를 두고 Ctrl+.을 누르면 또 자동으로 주르륵

Getter와 Setter를 만든다.

그럼 이런 모양의 클래스가 만들어진다.

// src/main/java/hello/hellospring/Member.java
package hello.hellospring.domain;

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

3. 회원 리포지토리 만들기

src/main/java/hello/hellospringrepository 패키지(폴더)를 만들고, 그 안에 MemberRepository.java 파일을 만든다.

이번에는 interface로 선택해서 아래 내용을 채운다.

// src/main/java/hello/hellospring/MemberRepository.java
public interface MemberRepository {
    Member save(Member member);
    Optional<Member> findById(Long id);
    Optional<Member> findByName(String name);
    List<Member> findAll();
}

🔸 틈새 Java

  • Interface : 다른 클래스를 작성할 때 기본이 되는 틀을 제공하면서, 다른 클래스 사이의 중간 매개 역할까지 담당하는 일종의 추상 클래스
  • class : 함수의 내용까지 선언 VS interface : 함수의 이름만 선언
  • Optional : null이 올 수 있는 값을 감싸는 Wrapper 클래스. null을 반환해도 에러가 발생하지 않도록 해준다.

repository 패키지 안에 MemoryMemberRepository.java 파일을 만든다.

MemberRepository를 상속한 MemoryMemberRepository 클래스를 만들고, Ctrl+.을 눌러서 인터페이스 안에 써놨던 메소드들을 선언해놓는다.

그리고 아래와 같이 각 메소드 안의 내용을 채운다.

// src/main/java/hello/hellospring/MemoryMemberRepository.java
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 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());
    }
}

🔸 틈새 Java

  • HashMap : 키와 값으로 구성된 Entry 객체를 저장하는 구조를 가지고 있는 자료구조로, 해시 함수를 통해 '키'와 '값'이 저장되는 위치를 결정한다. put(key, value)로 값을 추가한다.
  • ofNullable() : 명시된 값이 null이 아니면 명시된 값을 가지는 Optional 객체를 반환하고, 명시된 값이 null이면 비어있는 Optional 객체를 반환한다.
  • -> : 자바 람다표현식의 syntax 일부. (argument, ...) -> {expression}

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

- @Test

src/test/java/hello/hellospringrepository 패키지를 추가하고, MemoryMemberRepositoryTest.java 파일을 만든다.

// src/test/java/hello/hellospring/MemoryMemberRepositoryTest.java
class MemoryMemberRepositoryTest {
    @Test
    public void test() {
        
    }
}

이런식으로 메소드를 쓰고 위에 @Test를 붙여주면

이렇게 왼쪽에 삼각형이 생기는데, 얘를 누르면 해당 메소드가 실행된다. 실행했을 때 제대로 되면 초록색으로 체크 표시가 뜨고, 에러가 나면 빨갛게 된다.

이 문법을 따라서 위에서 작성한 메소드들을 테스트하는 코드를 다음과 같이 적는다.

// src/test/java/hello/hellospring/MemoryMemberRepositoryTest.java
class MemoryMemberRepositoryTest {
    MemberRepository repository = new MemoryMemberRepository();

    @Test
    public void save() {
        Member member = new Member();
        member.setName("spring");
        repository.save(member);
        Member result = repository.findById(member.getId()).get();
        // optional에서 값 꺼낼 때는 get()
        System.out.println("result = " + (result == member));   
    }

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

🔸 틈새 Java
인터페이스명 변수명 = new 클래스명();

- @AfterEach

하나하나 실행하면 잘 되는데, class 옆에 있는 거를 눌러서 한번에 실행시키면 오류가 난다.

이거는 테스트코드가 돌아가는 순서가 랜덤이라 값이 한번 쓰이고 비워지지 않아서 오류가 나는건데, 이를 방지 하기 위해서 아래 함수 2개를 추가해준다.

// src/test/java/hello/hellospring/MemoryMemberRepositoryTest.java
class MemoryMemberRepositoryTest {
    ...
    @AfterEach
    public void afterEach() {
        repository.clearStore();
    }
    ...
}
// src/main/java/hello/hellospring/MemoryMemberRepository.java
public class MemoryMemberRepository implements MemberRepository{
	...
	public void clearStore(){
        store.clear();
    }
    ...
}

이러고 다시 돌리면 에러가 나지 않고 모두 제대로 실행된다.


5. 회원 서비스 개발

src/main/java/hello/hellospringservice 패키지를 만들고 MemberService.java 파일을 만든다.

아래처럼 내용을 채운다.

// src/main/java/hello/hellospring/MemberService.java
public class MemberService {
    private final MemberRepository memberRepository;

	// 생성자 => 이후 발생한 오류 때문에 만들어줌
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    /* 회원가입 */
    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);
    }
}

- 메소드 추출하기

중복 회원 검증하는 부분만 따로 떼어보면,

일단 이렇게 쓴 다음에

메소드로 뺄 부분을 드래그 하고 Ctrl+.을 누르면 이렇게 뜬다.

Extract to method를 누르고 메소드 명을 입력하면

이렇게 메소드로 빠진다.

6. 회원 서비스 테스트

- 테스트 코드 만들기 단축키

테스트할 클래스 위에 커서를 대고 마우스 오른쪽을 클릭하고 Go to Test를 누른다.

딴딴딴 누르면

알아서 test 밑에 패키지랑 코드 뼈대를 만들어준다. 굿

대충 이런식으로 테스트해보고

// src/test/java/hello/hellospring/service/MemberServiceTest.java
public class MemberServiceTest {

    MemberService memberService = new MemberService();

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

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

        // then
        Member findMember = memberService.findOne(saveId).get();
        Assertions.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));
    }
}

- @BeforeEach

한번에 실행했을 때 각각 테스트 코드 실행 후 값이 비워지지 않아서 생기는 오류 발생 방지를 위해 아래 코드를 추가해준다.

// src/test/java/hello/hellospring/service/MemberServiceTest.java
public class MemberServiceTest {

    MemberService memberService;
    MemoryMemberRepository memberRepository;

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

    @AfterEach
    public void afterEach() {
        memberRepository.clearStore();
    }
}
  • @BeforeEach : 테스트 메소드 실행 이전에 수행
  • @AfterEach : 테스트 메소드 실행 이후에 수행

테스트 메소드 실행 전에 메소드마다 새로운 객체(레포지토리와 서비스)를 만들어주고, 실행 후에 레포지토리를 비운다.

4섹션 5번째 영상에서 @BeforEach로 감싸주는 이유가 궁금합니다!

profile
일기장같은 공부기록📝

0개의 댓글