spring - 입문2

» BE

인프런 김영한님 스프링 입문 강의를 듣고 정리한 내용입니다
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#

웹 개발 방식

  1. 정적 컨텐츠
  2. MVC 와 템플릿 엔진
  3. API

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

Image 컨트롤러 : MVC 의 컨트롤러
서비스 : 핵심 비즈니스 로직 구현(회원 중복 가입 x 등 )
리포지토리 : 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리 도메인 : 비즈니스 도메인 객체(회원, 주문, 쿠폰 등 데이터베이스에 저장되는 객체)


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

도메인

spring.hello.spring>domain>Member

package spring.hello.spring.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;
    }
    //getter setter
}

리포지토리

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);
    Optional<Member> findByName(String name);
    //NULL 이 리턴됐을 때의 예외를 쉽게 처리하기 위해서 Optional 사용
    List<Member> findAll();
}

리포지토리 구현체

repository/MemoryMemberRepository
📌 option + enter로 implement 쉽게 구현
📌 임포트는 option + enter 또는 ctrl + space

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

import java.util.*;

public class MemoryMemberRepository implements MemberRepository{

    private static Map<Long, Member> store = new HashMap<>();
    private static long sequence = 0L; //0,1,2 키값 생성
    @Override
    public Member save(Member member) {
        member.setId(++sequence); //id 세팅
        store.put(member.getId(), member); //스토어 저장
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(store.get(id)); //optional로 null이어도 감쌀 수 있음
    }

    @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()); // 전체 value(=Member)들을 리턴
    }
}

📌 java

  1. store.values() :
    • Map에 저장된 모든 value들을 Collection 형태로 반환
  2. .stream()
    • store.values()로 얻은 Collection을 스트림으로 변환
  3. .filter(member -> member.getName().equals(name))
    • 람다 표현식으로 member의 getName() 메서드를 호출해서 반환된 값이 name과 같은지를 판단
    • true인 요소들만 남기고 그렇지 않은 요소들은 스트림에서 제거해서 필터링
  4. .findAny()
    • 필터링된 스트림에서 임의의 하나의 요소를 찾아 반환
    • 반환 타입은 Optional<Member>
    • 찾은 요소가 있으면 해당 요소를 감싸서 반환하고 없으면 Optional.empty()를 반환

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

정상적으로 작동하는지 검증
MemoryMemberRepositoryTest.java

class MemoryMemberRepositoryTest {

    MemoryMemberRepository repository = new MemoryMemberRepository();

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

        repository.save(member);

        Member result = repository.findById(member.getId()).get(); // Optioanl에서 값을 꺼낼 때는 get()으로
        //System.out.println("result =" + (result == member)); 로도 확인 가능
        ////Assertions.assertEquals(member, result); 로도 확인 가능

        // test
        // member 와 result 가 같은지 검사
        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);

    }
}

📌 변수 rename은 ctrl + shift + R 또는 fn + shift + f6으로 한 번에 하기

✅ 전체 코드를 돌려보면 에러가 남 ->

  • 테스트 코드 메서드는 임의대로 실행
  • findAll() 메서드에서 repository 에 이름이 spring1 인 member1 을 save 했는데, 뒤이어 실행되는 findByName() 메서드에서 이름이 spring1 인 객체를 이용하여 테스트하고 있음
  • 서로 다른 테스트 메서드가 사용하는 데이터에서 충돌
  • 테스트메서드가 끝나면 사용한 데이터를 초기화 해줘야 함

MemoryMemberRepositoryTest.java

@AfterEach
    public void afterEach() {
        repository.clearStore();
    } //store 초기화

repository/MemoryMemberRepository.java

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

회원 서비스 개발

회원 가입

service/MemberService

public class MemberService {

	private final MemberRepository memberRepository = new MemoryMemberRepository();

	// 회원가입
	public Long join(Member member) {
		// 같은 이름이 있는 중복 회원 X
		memberRepository.findByName(member.getName())
				.ifPresent(m -> {
					throw new IllegalStateException("이미 존재하는 회원입니다.");
				}); // 이 방식 권장

		/* 
        Optional<Member> result = memberRepository.findByName(member.getName());
        
        result.ifPresent(m -> {
			throw new IllegalStateException("이미 존재하는 회원입니다.");
		}); // result가 null이 아닌 다른 값이 있으면 이미 존재하는 회원입니다 뜨게 하기
		*/

		memberRepository.save(member);
		return member.getId();

	}

    // 전체 회원 조회
	public List<Member> findMembers() {
		return memberRepository.findAll();
	}

	// 회원 한 명 조회
	public Optional<Member> findOne(Long memberId) {
		return memberRepository.findById(memberId);
	}
}

📌 cmd + option + v로 변수 추출 가능
📌 ctrl + t 하고 8번 extract method 하거나 그냥 바로 extract method(cmd + option + m) 하기 -> more option으로 함수명 설정

회원 서비스 테스트

MemberServiceTest.java
📌 cmd + shift + t로 테스트코드 바로 만들기 가능

class MemberServiceTest {

    MemberService memberService = new MemberService();
    MemoryMemberRepository memberRepository = new MemoryMemberRepository();

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

    @Test
    void 회원가입() { //test코드는 한글 가능
        //given
        Member member = new Member();
        member.setName("hello");

        //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);
        /*
        try {
            memberService.join(member2);
            fail();
        } catch (IllegalStateException e){
             assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
        }*/

        //then
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));

  • 두 번째 인자인 람다함수 실행 시, 첫 번째 인자값인 IllegalStateException이 발생하는지 확인

📌 ctrl + r로 이전에 실행했던 거 바로 실행
📌 cmd + b 로 navigate


MemberService.java

private final MemberRepository memberRepository;

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

✅ memberRepository를 직접 new로 생성하는 게 아니라 외부에서 넣어주도록 수정


MemberServiceTest.java

class MemberServiceTest {

    MemberService memberService;
    MemoryMemberRepository memberRepository; //1

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

각 테스트를 실행하기 전에 MemoryMemberRepository를 만들고 1에다 넣어줌, 걔를 memberService에 넣어줌, 같은 MemoryMemberRepository 사용 -> DI