회원 관리 프로젝트 Part1

Membership Management Requirements

코드 작성

**getter.setter를 이용한다.

모르는 문법 정리

domain package

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

repository package

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(Long name);
    List<Member> findAll();

}
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<>();
    //실무에서는 동시성 문제 때문에 공유되는 변수는 hashmap을 사용하지 않는다.
		//실무에서는 ConcurrentHashMap, AtomicLong 사용 고려
    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));
        //optional로 감싸면 값이 null이 와도 클라이언트에서 처리 가능
    }

    @Override
    public Optional<Member> findByname(Long name) {
        return store.values().stream()
                .filter(member -> member.getName().equals(name))
                .findAny();
    }

    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }
}

테스트 케이스 작성

개발한 기능을 main에서 해당 기능을 테스트 할 수 있다. 하지만 이와 같은 방법은 준비하고, 실행하는데 오래 걸리고, 반복 실행하기 어렵기 때문에 테스트를 이용한다.

Java는 JUnit이라는 프레임 워크로 테스트를 실행해서 이러한 문제를 해결

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

public 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();
				//Assertions.assertEquals(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);

    }
}
package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

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

//이 코드는 순수한 자바 코드이기에 spring이 이 코드가 특별한지 잘 모른다. 그리고
//autowire를 해도 연결이 안되는 이유가 이 부분이다.
@Service
//service라고 하여 spring 컨테이너에 등록해준다.
public class MemberService {

    /*
    *
    * 회원가입
    * */

//    private final MemberRepository memberRepository = new MemoryMemberRepository(); -> *****************
    private final MemberRepository memberRepository;

    @Autowired
    public MemberService(MemberRepository memberRepository){
        this.memberRepository = memberRepository;
    }
    public Long join(Member member){
        //같은 이름이 있는 중복 회원 X
//        Optional<Member> result = memberRepository.findByName(member.getName());
//        result.ifPresent(m -> {
//            throw new IllegalStateException("이미 존재하는 회원입니다.");
//        }); -> 좋은 코드 아님 어쩌피 반환값이 optonal이기 때문에 중복으로 할 이유가 없음
        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);
    }
}

DI

public class MemberService {
      private final MemberRepository memberRepository = new MemoryMemberRepository();
}

→ 이를 회원 서비스 코드를 DI 가능하게 변경한다. (외부에서 객체를 생성해서 주입 하는 방식, 의존성은 낮추고 결합도는 올림)

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

회원 서비스 테스트

package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
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
	public void 회원가입() throws Exception {
		//Given
    Member member = new Member();
    member.setName("hello");
		//When
    Long saveId = memberService.join(member);
		//Then
    Member findMember = memberRepository.findById(saveId).get();
    assertEquals(member.getName(), findMember.getName());
	}
	@Test
	public void 중복_회원_예외() throws Exception {
		//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));//예외가 발생해야 한다. 
		assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
	} 
}

@BeforeEach : 각 테스트 실행 전에 실행됨, 테스트가 서로 영향이 없도록 항상 새로운 객체를 생성하고, 의존 관계도 맺어줌

Spring bean과 외존 관계

컴포넌트 스캔과 자동 의존 관계 설정

회원 컨트롤러와 회원 서비스와 회원 리포지토리를 사용할 수 있게 외존 관계를 준비

package hello.hellospring.controller;
  import hello.hellospring.service.MemberService;
  import org.springframework.beans.factory.annotation.Autowired;
  import org.springframework.stereotype.Controller;
  
	@Controller
  public class MemberController {
      private final MemberService memberService;
      @Autowired
      public MemberController(MemberService memberService) {
					this.memberService = memberService;
      }
}

오류 발생

Screen_Shot_2022-05-09_at_11.20.30_AM.png
Screen_Shot_2022-05-09_at_11.20.44_AM.png
memberService가 스프링 빈으로 등록되어있지 않다.

Controller는 스프링에서 제공하는 컨트롤러여서 스프링 빈으로 자동 등록된다.(@Controller가 있으면 자동 등록됨)

컴포넌트 스캔 원리

@Service
    public class MemberService {
        private final MemberRepository memberRepository;
        @Autowired
        public MemberService(MemberRepository memberRepository) {
            this.memberRepository = memberRepository;
        }
}

생성자에 @Autowired를 사용하면 객체 생성 시점에 스프링 컨테이너에서 해당 스프링 빈을 찾아서 주입한다. 생성자가 1개만 있으면 @Autowired는 생략 가능

@Repository
    public class MemoryMemberRepository implements MemberRepository {}

Screen_Shot_2022-05-09_at_11.32.24_AM.png

스프링은 스프링 컨테이너에 스프링 빈을 등록할 때 기본으로 싱글톤으로 등록한다.(유일하기 하나만 등록) 따라서 같은 스프링 빈이면 모두 같은 인스턴스다. 설정으로 싱글톤이 아니게 설정할 수있지만, 특별한 경우를 제외하면 대부분 싱글톤을 사용한다.

자바 코드로 직접 스프링 빈 등록하기

package hello.hellospring.service;

import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {

    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository());
    }
    //시작할 때 Configuration을 읽고, Bean을 읽은 후에 Spring Bean에 자동으로 등록해준다.

    @Bean
    public MemberRepository memberRepository(){
        return new MemoryMemberRepository();
    }
}

** Contorller를 제외하고는 Service, Repository 애노테이션을 제거 해야지 에러가 생기지 않는다 **

MemberContoller의 DI

@Controller
public class MemberController {

    private final MemberService memberService;//final을 적으면 setter 사용 불가
    //new 객체를 사용하지 않는 이유
    //membercontroller를 제외하고 다른 모든 컨트롤러들이 member -> 큰 기능이 없기 때문에 여러개를 만들기 보단
    //공용으로 만들어서 공유하는게 좋다

    @Autowired private MemberService memberService; -> 필드 주입방법(좋은 방법은 아님, 변경할 수 가 없다.)

    @Autowired
    public void setMemberService(MemberService memberService){
        this.memberService = memberService;
    }

    //public으로 되어잇기 때문에 변경햇을 시에 노출 된다는 단점이 있다, 그러다보니 거의 대부분은 생성자 주입을 권장

    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }
    //Autowired : spring이 멤버서비스와 멤버 컨트롤러와 자동으로 연결 시켜준다
    //생성자를 생성해서 주입 하는 방식임

}

#Java #Spring_Java