diff --git a/src/main/java/hello/hellospring/HelloSpringApplication.java b/src/main/java/hello/hellospring/HelloSpringApplication.java new file mode 100644 index 0000000..f0f0fd4 --- /dev/null +++ b/src/main/java/hello/hellospring/HelloSpringApplication.java @@ -0,0 +1,13 @@ +package hello.hellospring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class HelloSpringApplication { + + public static void main(String[] args) { + SpringApplication.run(HelloSpringApplication.class, args); + } + +} diff --git a/src/main/java/hello/hellospring/controller/HelloController.java b/src/main/java/hello/hellospring/controller/HelloController.java new file mode 100644 index 0000000..f90855d --- /dev/null +++ b/src/main/java/hello/hellospring/controller/HelloController.java @@ -0,0 +1,57 @@ +package hello.hellospring.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +public class HelloController { + @GetMapping("hello") + public String hello(Model model) { + model.addAttribute("data", "hello!!"); + return "hello"; + } + + @GetMapping("hello-mvc") + public String helloMcv(@RequestParam("name") String name, Model model) { + model.addAttribute("name", name); + return "hello-template"; + } + + @GetMapping("hello-string") + @ResponseBody + public String helloString(@RequestParam("name") String name) { + return "hello " + name; + } + + @GetMapping("hello-api") + @ResponseBody + public Hello helloApi(@RequestParam("name") String name) { + Hello hello = new Hello(); + hello.setName(name); + hello.setNum(100); + return hello; + } + + static class Hello { + private String name; + private int num; + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + + public void setNum(int num) { + this.num = num; + } + + public int getNum() { + return num; + } + } + +} diff --git a/src/main/java/hello/hellospring/domain/Member.java b/src/main/java/hello/hellospring/domain/Member.java new file mode 100644 index 0000000..87b09ac --- /dev/null +++ b/src/main/java/hello/hellospring/domain/Member.java @@ -0,0 +1,22 @@ +package hello.hellospring.domain; + +public class Member { + private Long id; // 데이터를 구분하기 위해 시스템이 저장하는 id + private String name; + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public void setId(Long id) { + this.id = id; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/hello/hellospring/repository/MemberRepository.java b/src/main/java/hello/hellospring/repository/MemberRepository.java new file mode 100644 index 0000000..9961ba6 --- /dev/null +++ b/src/main/java/hello/hellospring/repository/MemberRepository.java @@ -0,0 +1,18 @@ +package hello.hellospring.repository; + +// 회원 객체를 저장하는 repository + +import hello.hellospring.domain.Member; + +import javax.swing.text.html.Option; +import java.util.List; +import java.util.Optional; + +public interface MemberRepository { + Member save(Member member); // 회원 저장 -> 저장된 회원 반환 + Optional findById(Long id); // ID로 member를 찾음 + // Optional: 없으면 NULL인데, NULL을 반환할 때 Optional로 감싸서 반환 + Optional findByName(String name); // name으로 menber를 찾음 + List findAll(); // 전체 member 반환 + +} diff --git a/src/main/java/hello/hellospring/repository/MemoryMemberRepository.java b/src/main/java/hello/hellospring/repository/MemoryMemberRepository.java new file mode 100644 index 0000000..186d995 --- /dev/null +++ b/src/main/java/hello/hellospring/repository/MemoryMemberRepository.java @@ -0,0 +1,39 @@ +package hello.hellospring.repository; + +import hello.hellospring.domain.Member; + +import java.util.*; + +public class MemoryMemberRepository implements MemberRepository { + + // Map에 member save + private static Map store = new HashMap<>(); + private static long sequence = 0L; // key 값(id)을 생성 + @Override + public Member save(Member member) { + member.setId((++sequence)); + store.put(member.getId(), member); // store(member가 save되는 MAP)에 member 저장 + return member; + } + + @Override + public Optional findById(Long id) { + return Optional.ofNullable(store.get(id)); // NULL일 가능성이 있으면 Optional로 감싸서 반환 + } + + @Override + public Optional findByName(String name) { + return store.values().stream() + .filter(member -> member.getName().equals(name)) + .findAny(); + } + + @Override + public List findAll() { + return new ArrayList<>(store.values()); + } + + public void clearStore(){ + store.clear(); + } +} diff --git a/src/main/java/hello/hellospring/service/MemberService.java b/src/main/java/hello/hellospring/service/MemberService.java new file mode 100644 index 0000000..615d02c --- /dev/null +++ b/src/main/java/hello/hellospring/service/MemberService.java @@ -0,0 +1,46 @@ +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); + return member.getId(); + } + + private void validateDuplicateMember(Member member) { + // 같은 이름이 있는 중복 회원 X + memberRepository.findByName(member.getName()) // Optional member를 반환 + .ifPresent(m -> { // ifPresent: 값이 있으면 수행 + throw new IllegalStateException("이미 존재하는 회원입니다."); // 에러 발생 + }); + } + + /** + * 전체 회원 조회 + */ + public List findMembers(){ + return memberRepository.findAll(); + } + + public Optional findOne(Long memberId){ + return memberRepository.findById(memberId); + + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/src/main/resources/static/hello-static.html b/src/main/resources/static/hello-static.html new file mode 100644 index 0000000..55f9563 --- /dev/null +++ b/src/main/resources/static/hello-static.html @@ -0,0 +1,10 @@ + + + + static content + + + +정적 컨텐츠 입니다. + + \ No newline at end of file diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html new file mode 100644 index 0000000..795f1e6 --- /dev/null +++ b/src/main/resources/static/index.html @@ -0,0 +1,12 @@ + + + + + Hello + + + + Hello + hello + + \ No newline at end of file diff --git a/src/main/resources/templates/hello-template.html b/src/main/resources/templates/hello-template.html new file mode 100644 index 0000000..18141fb --- /dev/null +++ b/src/main/resources/templates/hello-template.html @@ -0,0 +1,5 @@ + + +

hello! empty

+ + \ No newline at end of file diff --git a/src/main/resources/templates/hello.html b/src/main/resources/templates/hello.html new file mode 100644 index 0000000..06edf9b --- /dev/null +++ b/src/main/resources/templates/hello.html @@ -0,0 +1,10 @@ + + + + Hello + + + +

안녕하세요. 손님

+ + \ No newline at end of file diff --git a/src/test/java/hello/hellospring/HelloSpringApplicationTests.java b/src/test/java/hello/hellospring/HelloSpringApplicationTests.java new file mode 100644 index 0000000..87d37cf --- /dev/null +++ b/src/test/java/hello/hellospring/HelloSpringApplicationTests.java @@ -0,0 +1,13 @@ +package hello.hellospring; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class HelloSpringApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/src/test/java/hello/hellospring/repository/MemoryMemberRepositoryTest.java b/src/test/java/hello/hellospring/repository/MemoryMemberRepositoryTest.java new file mode 100644 index 0000000..968008f --- /dev/null +++ b/src/test/java/hello/hellospring/repository/MemoryMemberRepositoryTest.java @@ -0,0 +1,63 @@ +package hello.hellospring.repository; + +import hello.hellospring.domain.Member; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class MemoryMemberRepositoryTest { // 다른 데서 가져다 쓸 게 아니므로 public이 아니어도 됨 + MemoryMemberRepository repository = new MemoryMemberRepository(); + + // test가 끝날 때마다 repository를 깔끔하게 지워주는 코드 + @AfterEach // 동작이 끝날 때마다 수행한다는 의미 + public void afterEach(){ + repository.clearStore(); + } + + @Test // save() 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)); + Assertions.assertEquals(result, member); // result와 member가 같은지 확인(1) (다르면 에러) + assertThat(member).isEqualTo(result);// result와 member가 같은지 확인(2) (다르면 에러) + } + + @Test // 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); // 다르면 에러 + } + + @Test // findAll() 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 result = repository.findAll(); + + assertThat(result.size()).isEqualTo(2); // member 수와 일치하지 않으면 에러 + } +} diff --git a/src/test/java/hello/hellospring/service/MemberServiceTest.java b/src/test/java/hello/hellospring/service/MemberServiceTest.java new file mode 100644 index 0000000..e042130 --- /dev/null +++ b/src/test/java/hello/hellospring/service/MemberServiceTest.java @@ -0,0 +1,68 @@ +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.AssertionsForClassTypes.assertThat; +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 회원가입() { // test code는 한글로 적어도 됨 + // 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 중복_회원_예외(){ // 이름이 중복되는 회원이 가입하려고 할 때 예외 처리가 정상적으로 수행되는지 확인 + // given + Member member1 = new Member(); + member1.setName("spring"); + + Member member2 = new Member(); + member2.setName("spring"); // member 이름이 중복됨 + + // when + memberService.join(member1); + IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));// A -> B: B가 실행되면 A가 터져야 함 + + assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다."); // 중복 회원이 발생했을 때 이 메시지가 정상적으로 발생하는지 확인 + + + // then + } + @Test + void findMembers() { + } + + @Test + void findOne() { + } +} \ No newline at end of file diff --git "a/\354\204\271\354\205\230 3. \355\232\214\354\233\220 \352\260\200\354\236\205 \354\230\210\354\240\234.md" "b/\354\204\271\354\205\230 3. \355\232\214\354\233\220 \352\260\200\354\236\205 \354\230\210\354\240\234.md" new file mode 100644 index 0000000..f70fc5c --- /dev/null +++ "b/\354\204\271\354\205\230 3. \355\232\214\354\233\220 \352\260\200\354\236\205 \354\230\210\354\240\234.md" @@ -0,0 +1,309 @@ +# 섹션 3. 회원 관리 예제 +## 1. MVC 패턴 +### MVC 패턴 + +![image](https://github.com/Learn-Java-Spring-Team3/spring-study/assets/128376848/a816992d-e1a8-476a-8f8a-4080fd84381e) + +사용자가 controller를 조작 + +-> controller는 model을 통해서 데이터를 가져옴 + +-> controller는 그 정보를 바탕으로 시각적인 표현을 담당하는 View를 제어해서 사용자에게 전달 + + +- model: data의 가공을 책임짐 (데이터베이스, 처음의 정의하는 상수, 초기화값, 변수 등) +- view: input 텍스트, 체크박스 항목 등과 같은 사용자 인터페이스 요소 (input/output) +- controller: data-user 인터페이스를 잇는 다리 역할 (사용자가 발생시키는 이벤트를 처리하는 역할) + +### spring의 MVC +![image](https://github.com/Learn-Java-Spring-Team3/spring-study/assets/128376848/474e9dd1-787a-4acf-b9c2-2ec21c8d8467) + +----- + +## 2. 비즈니스 요구사항 정리 +- data: 회원 ID, 이름 +- 기능: 회원 등록, 조회 + +![image](https://github.com/Learn-Java-Spring-Team3/spring-study/assets/128376848/3b001b8e-c419-4330-a668-8bd1363aecfe) + +- 컨트롤러 + * MVC(Model-View-Controller) + * Controller는 화면(View)와 비즈니스 로직(Model)을 연결시키는 역할 + * 사용자가 화면(view)에서 입력 등의 이벤트를 함 -> 그 이벤트에 맞는 화면(view)나 비즈니스 로직(Model)을 실행할 수 있도록 업데이트 +- 서비스 + * 핵심 비즈니스 로직 (비즈니스 도메인 객체 사용하여 구성) + * eg) 회원은 중복 가입이 안 된다. +- 도메인 + * 비즈니스 도메인 객체 + * 데이터베이스에 저장하고 관리됨 + * eg) 회원, 주문, 쿠폰 등 +- 리포지토리 + * 데이터베이스에 접근 + * 도메인 객체를 DB에 저장하고 관리 + + ![image](https://github.com/Learn-Java-Spring-Team3/spring-study/assets/128376848/cb1ef2fb-913c-485a-899a-20133ba3ae32) +- 아직 데이터 저장소가 선정되지 않아서, 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계 +- 데이터 저장소는 RDB, NoSQL 등등 다양한 저장소를 고민중인 상황으로 가정 +- 개발을 진행하기 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용 +----- +## 3. 회원 도메인과 레포지토리 만들기 + +### 1) 사용할 데이터(= 멤버) 선언 [Member.java] + +src - main - java - hello.hello-spring - domain - Member + + 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; + } + } +### 2) 기능 선언 [MemberRepository.java] + +src - main - java - hello.hello-spring - repository - MemberRepository (interface) + + package hello.hellospring.repository; + + // 회원 객체를 저장하는 repository + + import hello.hellospring.domain.Member; + + import javax.swing.text.html.Option; + import java.util.List; + import java.util.Optional; + + public interface MemberRepository { + Member save(Member member); // 회원 저장 -> 저장된 회원 반환 + Optional findById(Long id); // ID로 member를 찾음 + // Optional: 없으면 NULL인데, NULL을 반환할 때 Optional로 감싸서 반환 + Optional findByName(String name); // name으로 menber를 찾음 + List findAll(); // 전체 member 반환 + + } + +### 3) 기능 구현[MemoryMemberRepository.java] + +rc - main - java - hello.hello-spring - repository - MemoryMemberRepository (class) + + package hello.hellospring.repository; + + import hello.hellospring.domain.Member; + + import java.util.*; + + public abstract class MemoryMemberRepository implements MemberRepository { + + // Map에 member save + private static Map store = new HashMap<>(); + private static long sequence = 0L; // key 값(id)을 생성 + @Override + public Member save(Member member) { + member.setId((++sequence)); + store.put(member.getId(), member); // store(member가 save되는 MAP)에 member 저장 + return member; + } + + @Override + public Optional findById(Long id) { + return Optional.ofNullable(store.get(id)); // NULL일 가능성이 있으면 Optional로 감싸서 반환 + } + + @Override + public Optional findByName(String name) { + return store.values().stream() + .filter(member -> member.getName().equals(name)) + .findAny(); + } + + @Override + public List findAll() { + return new ArrayList<>(store.values()); + } + } + +----- +## 4. 회원 레포지토리 테스트 케이스 작성 [MemoryMemberRepositoryTest.java] + +1) main 메서드로 실행 +2) 웹 어플리케이션의 controller +3) JUnit이라는 프레임워크로 test code를 만들어서 test code를 실행 (★) + +src - test - java - hello - hellospring - repository - MemoryMemberRepositoryTest + + +![image](https://github.com/Learn-Java-Spring-Team3/spring-study/assets/128376848/8a6d4129-16f6-44bd-be3d-9c136b989ec6) + +-> 이걸 누르면 동시에 test를 모두 실행할 수 있음 + + package hello.hellospring.repository; + + import hello.hellospring.domain.Member; + import org.junit.jupiter.api.AfterAll; + import org.junit.jupiter.api.AfterEach; + import org.junit.jupiter.api.Assertions; + import org.junit.jupiter.api.Test; + + import java.util.List; + + import static org.assertj.core.api.Assertions.assertThat; + + class MemoryMemberRepositoryTest { // 다른 데서 가져다 쓸 게 아니므로 public이 아니어도 됨 + MemoryMemberRepository repository = new MemoryMemberRepository(); + + // test가 끝날 때마다 repository를 깔끔하게 지워주는 코드 + @AfterEach // 동작이 끝날 때마다 수행한다는 의미 + public void afterEach(){ + repository.clearStore(); + } + + @Test // save() 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)); + Assertions.assertEquals(result, member); // result와 member가 같은지 확인(1) (다르면 에러) + assertThat(member).isEqualTo(result);// result와 member가 같은지 확인(2) (다르면 에러) + } + + @Test // 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); // 다르면 에러 + } + + @Test // findAll() 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 result = repository.findAll(); + + assertThat(result.size()).isEqualTo(2); // member 수와 일치하지 않으면 에러 + } + } + +cf) test가 한번 끝나면 repository를 지워줘야 함 +- MemoryMemberRepositoryTest.java (src - test - java - hello - hellospring - repository) + + @AfterEach // 동작이 끝날 때마다 수행한다는 의미 + public void afterEach(){ + repository.clearStore(); + } + +- MemoryMemberRepository.java (src - main - java - hello - hellospring - repository) + + public void clearStore(){ + store.clear(); + } + + +----- +## 5. 회원 서비스 개발 + package hello.hellospring.service; + import hello.hellospring.domain.Member; + import hello.hellospring.repository.MemberRepository; + 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 findMembers() { + return memberRepository.findAll(); + } + public Optional findOne(Long memberId) { + return memberRepository.findById(memberId); + } + } +----- +## 6. 회원 서비스 테스트 + + 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("이미 존재하는 회원입니다."); + } + } diff --git "a/\354\204\271\354\205\230 6. \354\212\244\355\224\204\353\247\201 DB \354\240\221\352\267\274 \352\270\260\354\210\240.md" "b/\354\204\271\354\205\230 6. \354\212\244\355\224\204\353\247\201 DB \354\240\221\352\267\274 \352\270\260\354\210\240.md" new file mode 100644 index 0000000..7e8c224 --- /dev/null +++ "b/\354\204\271\354\205\230 6. \354\212\244\355\224\204\353\247\201 DB \354\240\221\352\267\274 \352\270\260\354\210\240.md" @@ -0,0 +1,394 @@ +# 섹션6. 스프링 DB 접근 기술 +- 순수 JDBC +- 스프링 통합 테스트 +- 스프링 JDBC template +- JPA + +## 순수 JDBC +어플리케이션과 데이터를 연결하는데, DB에 insert/select query를 날려서 데이터를 저장하는 방법 + +순수 JDBC: 20년 전 방법 (참고만 하면 된다.) + +1. JdbcMemberRepository 생성 +<소스 코드> +2. SpringConfig 변경 +<소스 코드> + +cf) 개방 폐쇄 원칙(OCP): 확장에는 열려 있고, 수정/변경에는 닫혀 있다. + +-> 기능을 완전히 변경해도, 애플리케이션이 동작하는 데 필요한 코드를 수정할 필요 X (조립하는 코드만 수정하면 됨) + +-> 스프링의 Dependency Injection에 의해 OCP가 만족됨 + +## 스프링 통합 테스트 +스프링 컨테이너와 DB를 연결한 통합 테스트 + +(지금까지의 test는 순수 Java에 대한 test, 지금의 코드는 Spring이 DB를 붙들고 있기 때문에 앞선 방식으로 test를 진행할 수 없고, 다른 방식으로 test를 진행해야 함) + +1. 기존의 MemberServiceTest.java를 복제하여 MemberServiceIngergationTest.java 파일을 만든다. + +test-java-hello.hellospring-service-MemberServiceIngrationTest.java + + +```java + + 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.AssertionsForClassTypes.assertThat; + import static org.junit.jupiter.api.Assertions.assertThrows; + + class MemberServiceIntegrationTest { + + MemberService memberService; + MemoryMemberRepository memberRepository; + + @BeforeEach // 각 테스트를 사용하기 전에 같은 메모리 레포지토리를 사용하게 함 + public void beforeEach(){ + memberRepository = new MemoryMemberRepository(); + memberService = new MemberService(memberRepository); + } + + @AfterEach // 동작이 끝날 때마다 수행한다는 의미 + public void afterEach(){ + memberRepository.clearStore(); + } + @Test + void 회원가입() { // test code는 한글로 적어도 됨 + // 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 중복_회원_예외(){ // 이름이 중복되는 회원이 가입하려고 할 때 예외 처리가 정상적으로 수행되는지 확인 + // given + Member member1 = new Member(); + member1.setName("spring"); + + Member member2 = new Member(); + member2.setName("spring"); // member 이름이 중복됨 + + // when + memberService.join(member1); + IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));// A -> B: B가 실행되면 A가 터져야 함 + + assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다."); // 중복 회원이 발생했을 때 이 메시지가 정상적으로 발생하는지 확인 + + + // then + } + @Test + void findMembers() { + } + + @Test + void findOne() { + } + } +``` + + +2. MemberServiceIngergationTest.java 코드 수정 + cf) @SpringBootTest: 스프링 컨테이나와 테스트를 함께 실행함. + + cf) @Transactional: 이걸 test case에 달면, test를 실행할 때, + transactional을 먼저 실행하고, DB에 데이터를 다 넣은 다음에, test가 끝나면 롤백을 해줌. + 즉, DB에 넣었던 데이터가 반영이 안 되고 지워짐. (다음 테스트에 영향 X) +```java +package hello.hellospring.service; +import hello.hellospring.domain.Member; +import hello.hellospring.repository.MemberRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SpringBootTest +@Transactional +class MemberServiceIntegrationTest { + @Autowired MemberService memberService; + @Autowired MemberRepository memberRepository; + + @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("이미 존재하는 회원입니다."); + } +} +``` + +## 스프링 JdbcTemplate +- Jdbc API에서 중복을 줄여줌. +- SQL은 직접 작성해야 함. + +1. JdbcTemplateMemberRepository.java 생성 +2. MemberRepository implements +3. JdbcTemplate jdbcTemplate -> JdbcTemplateMemberRepository 함수에 DataSource dataSorce를 인젝션 받음 +4. implement 받은 MemberRepository의 함수들을 jdbcTemplate을 사용해서 수정 + +main-java-hello.hellospring-repository-JdbcTemplateMemberRepository +```java +package hello.hellospring.repository; +import hello.hellospring.domain.Member; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import javax.sql.DataSource; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +public class JdbcTemplateMemberRepository implements MemberRepository { + private final JdbcTemplate jdbcTemplate; + public JdbcTemplateMemberRepository(DataSource dataSource) { + jdbcTemplate = new JdbcTemplate(dataSource); + } + @Override + public Member save(Member member) { + SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate); + jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id"); + Map parameters = new HashMap<>(); + parameters.put("name", member.getName()); + Number key = jdbcInsert.executeAndReturnKey(new + MapSqlParameterSource(parameters)); + member.setId(key.longValue()); + return member; + } + @Override + public Optional findById(Long id) { + List result = jdbcTemplate.query("select * from member where id + = ?", memberRowMapper(), id); + return result.stream().findAny(); + } + @Override + public List findAll() { + return jdbcTemplate.query("select * from member", memberRowMapper()); + } + @Override + public Optional findByName(String name) { + List result = jdbcTemplate.query("select * from member where + name = ?", memberRowMapper(), name); + return result.stream().findAny(); + } + private RowMapper memberRowMapper() { + return (rs, rowNum) -> { + Member member = new Member(); + member.setId(rs.getLong("id")); + member.setName(rs.getString("name")); + return member; + }; + } +} +``` + + +6. SpringConfig의 MemberRepository 함수를 JdacTemplateMemperRepository(dataSource)로 바꿈 +```java +public MemberRepository memberRepository() { + // return new MemoryMemberRepository(); + // return new JdbcMemberRepository(dataSource); + return new JdbcTemplateMemberRepository(dataSource); + } +``` +cf) 생성자가 하나만 있으면 @Autowired를 생략할 수 있음 +cf) Jdbc에서 try-catch의 긴 코드로 이루어진 부분을 스프링의 Jdbc template에서는 jdbcTemplate 라이브러리로 간단하게 구현됨 + +## JPA +- Jdbc API에서 중복을 줄여줌 +- 기본적인 SQL도 직접 만들어서 실행해줌 +(객체를 메모리에 넣듯이 JPA에 넣으면, DB에 넣고/가져오는 동작을 JPA가 실행해줌) + +1. build.grandle에 jpa 추가 + +hello-spring - build.grandle +```java +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-web' + //implementation 'org.springframework.boot:spring-boot-starter-jdbc' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + runtimeOnly 'com.h2database:h2' + testImplementation('org.springframework.boot:spring-boot-starter-test') { + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' + } +} +``` + +2. application.properties에 JPA 설정 추가 +```java +spring.datasource.url=jdbc:h2:tcp://localhost/~/test +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.username=sa +spring.jpa.show-sql=true +spring.jpa.hibernate.ddl-auto=none +``` + +cf) show-sql: JPA가 생성하는 SQL을 출력 + +cf) ddl-auto=none: 이미 테이블이 만들어져 있기 때문에 자동으로 테이블 생성 기능은 끔 + +3. Entity Mapping + +cf) JPA는 인터페이스고, 구현은 여러 업체들이 함 + +cf) JPA: ORM 기술 (object와 relational data base table을 mapping함. 어떻게? 어노테이션으) + +main - java - hello.hellospring - domain - Member +```java +package hello.hellospring.domain; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +@Entity +public class Member { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + 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; + } +} +``` + +4. 리포지토리 생성 + +main - java - hello.hellospring - repository +cf) JPA는 EntitiyManager로 모든 동작을 함 -> MemberRepository를 implements 받아 EntitiyManager를 사용하는 방식으로 override + +```java +package hello.hellospring.repository; +import hello.hellospring.domain.Member; +import javax.persistence.EntityManager; +import java.util.List; +import java.util.Optional; +public class JpaMemberRepository implements MemberRepository { + private final EntityManager em; + public JpaMemberRepository(EntityManager em) { + this.em = em; + } + public Member save(Member member) { + em.persist(member); + return member; + } + public Optional findById(Long id) { + Member member = em.find(Member.class, id); + return Optional.ofNullable(member); + } + public List findAll() { + return em.createQuery("select m from Member m", Member.class) + .getResultList(); + } + public Optional findByName(String name) { + List result = em.createQuery("select m from Member m where + m.name = :name", Member.class) + .setParameter("name", name) + .getResultList(); + return result.stream().findAny(); +} +} +``` + +5. Transaction 추가 +JPA를 쓰려면 (데이터를 저장/변경할 때) 항상 Transaction 필요 +```java +import org.springframework.transaction.annotation.Transactional +@Transactional +public class MemberService { ... +``` + +6. SpringConfig에서 Spring 설정 변경 + +cf) MemberRepository를 JpaMemberRepository로 변경 + +cf) EntitiyManager 사용 + +```java +package hello.hellospring; +import hello.hellospring.repository.*; +import hello.hellospring.service.MemberService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import javax.persistence.EntityManager; +import javax.sql.DataSource; +@Configuration +public class SpringConfig { + private final DataSource dataSource; + private final EntityManager em; + public SpringConfig(DataSource dataSource, EntityManager em) { + this.dataSource = dataSource; + this.em = em; + } + @Bean + public MemberService memberService() { + return new MemberService(memberRepository()); + } + @Bean + public MemberRepository memberRepository() { + // return new MemoryMemberRepository(); + // return new JdbcMemberRepository(dataSource); + // return new JdbcTemplateMemberRepository(dataSource); + return new JpaMemberRepository(em); + } +} +``` + + + + +