<스프링입문> 섹션 3, 섹션 4
인프런 <스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술> by 김영한
들어가기전에 자주 쓰는 인텔리제이 단축어 정리부터 하겠음
Alt + Insert : 코드 자동완성 (getter, setter, constructor...)
Alt + Enter : import 같은거 자동 생성
Ctrl + Alt + V : 변수명+타입 자동 생성
Ctrl + Alt + M : method로 자동 변경
Ctrl + Shift + T : test case 틀 자동 생성
Ctrl + Shift + Enter : 코드 끝 자동 완성 => 괄호 닫고 세미콜론 붙이고 엔터치고 안해도 됨
Shift + F6 : rename, 변수 이름 한꺼번에 변경 가능
/** + Enter : 문서 주석
섹션 3 : 회원 관리 예제 - 백엔드 개발
비즈니스 요구사항 정리
개발하고자 하는 패키지 밑에 controller, service, repository, domain 패키지를 설정
각 패키지 내부에서 자바 클래스를 생성해서, 원하는 구현을 한다.
현재 초기단계의 개발 설정
repository의 변경가능성을 염두에 두고, interface를 미리 만든 후, 그걸 implements 하는 가벼운 repo 구현.
회원 도메인과 리포지토리 만들기
Domain
package hello.hellospring.domain;
public class Member {
private Long id; // 고객이 정하는 아이디가 아닌, 데이터 구분을 위해 시스템이 저장하는 아이디
private String name; // 이름
// alt + insert : 코드 자동 생성
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;
}
}
기본적으로 사용할 정보들에 대한 domain
Repository
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.List;
import java.util.Optional;
// 회원 정보를 저장할 repository
public interface MemberRepository {
Member save(Member member); //회원 저장하면 회원 정보 반환
/* Optional : 자바8에 새로 도입된 기능
찾았을 때, Null 이면, Null을 반환하는 대신 optional로 감싸서 반환하는 방식
*/
Optional<Member> findById(Long id); // id로 회원을 찾는 기능
Optional<Member> findByName(String name); // 이름으로 회원을 찾는 기능
List<Member> findAll(); // 저장된 모든 회원 리스트 반환
}
repository에서 사용할 method 에 대한 interface
Optional
null 값이 나올 수 있는 method에 대해 미리 한번 감싸고 + 그 method에 대한 추가 기능 얻기 편함
** Long 타입
=> long 타입과 비슷하지만, null값이 존재 + 후에 생성될 가능성 때문에 참조 타입인 Long으로 받아서 한번 감싸주는 느낌
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.stereotype.Repository;
import java.util.*;
//@Repository
public class MemoryMemberRepository implements MemberRepository{
private static Map<Long, Member> store = new HashMap<>(); //실무에서는 concurrency hash map 사용(공유변수라)
private static long sequence = 0L; //0, 1, 2.. 처럼 key 값을 생성해주는 변수
@Override
public Member save(Member member) {
// 이름은 user가 회원가입할 때 적어서 넘어오는거고, 이건 시스템 상에서 구분을 위한 id
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
// null 일 때, optional로 감싸서 반환해주면, 클라이언트 쪽에서 뭔가를 할 수 있다고 함.
return Optional.ofNullable(store.get(id));
}
@Override
public Optional<Member> findByName(String name) {
// lambda 이용
//store.values() : store의 멤버들
return store.values().stream()
.filter(member -> member.getName().equals(name)) // 인자로 넘어온 값이 member의 name과 같은지 비교
.findAny(); // 뭔가 같은걸 찾으면 반환
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
//실무에서 list를 더 많이 쓴다고 함, 그냥 하나 새로 만들고 거기에 멤버들을 넘겨줌
}
public void clearStore(){
store.clear();
}
}
interface에서 만들어진 틀을 가지고 override.
map, optional, lambda 이런 자바 문법적인 이슈들은 따로 공부해야 할 것 같다.
회원 리포지토리 테스트 케이스 작성
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
class MemoryMemberRepositoryTest { // test 할거라 굳이 public으로 안해도 됨
/* test 의 순서는 보장 x, 그냥 method 별로 따로 동작함
다른 method 에서 저장된 객체가 넘어와서 영향을 줄 수 있기 때문에,
method 별로 test가 끝나면 repository를 clear 해줘야 한다.
==> 각 test 는 서로 의존관계가 있으면 안된다
*/
MemoryMemberRepository repository = new MemoryMemberRepository();
@AfterEach // 각각 method가 끝났을 때 호출되는 것 ==> test가 끝날 때마다 저장소 비워주기
public void afterEach(){
repository.clearStore();
}
@Test // test 케이스
public void save(){
Member member = new Member();
member.setName("spring");
repository.save(member);
Member result = repository.findById(member.getId()).get();
// .get() => optional 형의 무언가를 가져올 때 사용, 좋은건 아닌데 testcase 작성할 때는, 그냥 간편하게 이렇게 씀
//.get()으로 가져오면 optional을 한번 까서 바로 가져올 수 있음
//Assertions.assertEquals(member, result); // jupiter 거
// 출력을 찍어서 일일히 맞는지 확인하는 것이 아니라, assertion을 사용해서 기댓값이랑 다르면 에러나게 설정
assertThat(member).isEqualTo(result); // assertj 거, 좀더 편하게 쓸 수 있다고 함.
//alt + enter 후 import 해주면 그냥 assertThat으로 간편하게 확인 가능하다.
// member가 result랑 같으면 통과, 다르면 에러
}
@Test
public void findByName(){
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
// shift + F6 : rename
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);
}
}
/* test 주도 개발 , TDD
==> test 케이스를 먼저 짜 놓고 뒤에 실제 구현을 하는 것
내가 만들어야 할 틀을 미리 정해놓고 시작하는 개발 방식
*/
알아야 할 이슈
1. test case 간의 독립
2. @AfterEach
3. @BeforeEach => 뒤에 나옴
4. assert
회원 서비스 개발
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;
// ctrl + shift + T : Test case 껍데기 자동으로 만들어줌
//@Service // 이거 써서 스프링이랑 연결
public class MemberService { // 실제 비즈니스 로직을 구현 => 서비스 쪽은 네이밍도 비즈니스스럽게 한다 ㅋㅋ
private final MemberRepository memberRepository;
//@Autowired
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
} // 이렇게 해줌으로서 각 서비스가 동일 repo를 사용할 수 있게 됨.
/**
* 회원 가입
*/ // 문서 주석 : /** + enter
public Long join(Member member){ // long type id를 반환 => null 일 가능성능 고려해서, 객체 참조 타입인 Long으로 받아준다
// 같은 이름이 있는 중복 회원 X
/*
Optional<Member> result = memberRepository.findByName(member.getName()); // ctrl + alt + v : 변수 추출
result.ifPresent(m -> { // optional의 method, 이걸 통해서 여러 편리한 기능 사용 가능
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
optional 주렁주렁 달린거 안예쁨
*/
/*
memberRepository.findByName(member.getName())
.ifPresent(m->{
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
이런식으로 바로 optional형 반환값에 method 이어 붙이는거 권장
근데? 이런걸 method로 뽑고싶다?
=> ctrl + alt + m
*/
vaildateDuplicateMember(member); // 중복 회원 검증
memberRepository.save(member);
return member.getId();
}
private void vaildateDuplicateMember(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);
}
}
repository가 개발자가 쓰는거라면,
service는 실제 비즈니스 로직이 들어가기 때문에 naming의 차이가 존재 한다.
알아야 할 이슈
1. 다른 클래스와 DI 관계일 때 => 생성자 주입 방식으로 concurrency 해결
2. 비즈니스 로직 상의 예외처리
=> optional 타입의 반환값을 optional 내부 method를 이용하여 여러 조작 가능
회원 서비스 테스트
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 java.util.Optional;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
class MemberServiceTest {
/*MemberService memberService = new MemberService();
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
// 밑에걸 사용하기 위해 여기 멤버도 하나 받아옴
// 근데 이건 다른 객체라서 문제가 생길 수 있음 */
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforEach(){
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
/* 각 test 실행 전에, 생성자로 repo를 넘겨줘서 서비스 내에서 repo 객체 생성
서비스 입장에서 이렇게 하는거를 Dependency Injection (DI) 라고 한다.
*/
@AfterEach
public void afterEach(){
memberRepository.clearStore();
}
@Test
void 회원가입() {
//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);
assertThrows(IllegalStateException.class, () -> memberService.join(member2));
// join에 member2를 넘겼을 때, 이 예외가 터지면 성공으로 간주하는 문법
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
// 이런식으로 message 뽑아서 검증하는 것도 가능
/* try{
memberService.join(member2);
fail();
} catch (IllegalStateException e){
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다. ㅋㅋ");
}*/
//then
}
@Test
void findMembers() {
}
@Test
void findOne() {
}
}
알아야 할 이슈
1. 동시성을 위한 @BeforeEach 설정
2. given, when, then 으로 test case 설정
3. 예외사항도 테스트 해보기
4. exception catch 방법
섹션 4 : 스프링 빈과 의존관계
스프링이 우리가 구현한 자바 코드를 스프링 컨테이너에 등록시켜서 사용함.
이 때, 스프링 컨테이너에 스프링 빈을 등록시키는 두가지 방법 존
1. 컴포넌트 스캔
2. Config 를 이용한 직접 등록
컴포넌트 스캔 및 자동 의존관계 설정
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 // 스프링 컨테이너에 멤버 서비스 연결 => 의존관계 주입 => DI (컴포넌트 스캔 방식)
public MemberController(MemberService memberService) { // 생성자 주입 방식 => 거의 이 방식만 쓴다.
this.memberService = memberService;
}
}
/*
컴포넌트 스캔 방식은 main이 있는 클래스의 패키지 내부에서 적용된 컴포넌트만 가능
main이 없는 다른 패키지에서 컴포넌트를 등록한다고 해서 등록이 되는건 아님 (스캔을 안함)
그치만 사실 @ComponantScan annotation이 있으면 찾아서 등록을 해준다.
스프링은 컨테이너에 스프링 빈을 등록할 때 기본적으로 >싱글톤< 으로 등록.
다른 스프링빈이 등록 돼서 이미 기등록된 빈을 사용하고 싶을 때, 다시 등록하는게 아니라, 공유자원으로 있는거 쓴다는 얘기
=> 웬만하면 싱글톤만 사용
*/
Config 사용
package hello.hellospring;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import hello.hellospring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration // 컴포넌트스캔 방식이 아닌, 직접 스프링에 등록하는 방법
public class SpringConfig {
@Bean
public MemberService memberService(){
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
}
/*
상황에 따라 구현 클래스를 변경해야 하는 상황에서 config를 사용
나중에 memorymemberrepository를 다른 repo를 바꾸기 위해.
==> 여러 코드를 바꾸지 않고 config만 손대서 손쉽게 변경 가능
*/