STS 로 Spring Boot 프로젝트 만들기. 5) 데이터 베이스 사용하기. CRUD 예제
참고서적 : [길벗] 스프링부트 프로그래밍 입문 - 쇼다 츠야노
Eclipse 에서 Visual Studio Code로 넘어오긴 했지만, 여전히 사용하는건 STS기 때문에 제목은 그대로 하였습니다.
당장 내일까지 VSCODE에 익숙해져야만 하는 저의 사정상 이번 포스팅부터 VSCODE를 사용하지만,
이클립스건, VSCODE건, IntelliJ 건 결국 똑같으니 코드만 변경하면서 포스팅 내용을 확인 하시면 됩니다!
JPA (Java Persistence API)를 사용해보려고 합니다.
책에서는 HSQLDB(애플리케이션에 DB를 내장), JTA, Spring ORM, Spring AOP를 사용해본다고 하는데, Spring Boot Starter Data JPA가 HSQLDB 를 제외한 다른 dependency 들은 모두 포함하고 있다고 합니다.
1. dependency 추가
pom.xml에 간단하게 dependency를 추가 합니다. 아래 내용을 추가 해서
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
저장을 하니
혹시나 걱정했었는데, 역시 VSCODE 에서도 바로바로 변경사항이 있을때 build를 할 수 있습니다.
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<scope>runtime</scope>
</dependency>
이어서 hsqldb도 추가 해 두었습니다. 왠지 2차 과제에서 실제 DB를 줄 거 같지는 않고, hsqldb 비스무리하게 처리하지 않을 까 하는 생각이 들었습니다. 프로그래머스에서 공식적으로 지정한 데이터 베이스는 MYSQL 8 버전 입니다.
2. Member Class 만들기.
package com.shane.boot;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name="member")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
public long getId() {
return this.id;
}
public void setId(long id) {
this.id = id;
}
private String name;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
private String memo;
public String getMemo() {
return this.memo;
}
public void setMemo(String memo) {
this.memo = memo;
}
}
Mybatis를 썼을때의 VO(Value Object) 개념입니다.
간단하게 Id, name, memo를 property로 갖고, 각각의 getter와 setter를 만들어 줬습니다.
3. Repository 클래스 작성하기
일단 base package에 repositories라는 package를 만들어 줍니다. 그러고 거기에 MyRepository 인터페이스를 하나 만들어 줬습니다. 복사할 수 있게 아래에 코드도 올려둡니다.
package com.shane.boot.repositories;
import com.shane.boot.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface MyRepository extends JpaRepository<Member, Long>{
}
4. 실제로 Repository 연동하기.
이제 컨트롤러를 만들어서 실제 repository와 연동해 사용 해 보도록 하겠습니다.
package com.shane.boot.controller;
import com.shane.boot.Member;
import com.shane.boot.repositories.MyRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class RepositoryTest {
@Autowired
MyRepository repository;
@GetMapping("repository")
public ModelAndView index(ModelAndView mav) {
mav.setViewName("repositoryTest");
Iterable<Member> list = repository.findAll();
mav.addObject("data", list);
return mav;
}
}
컨트롤러는 이런식으로 작성됩니다. Mybatis에서는 CRUD 할때 쿼리를 작성해야 했는데, 그럴 필요 없이 .findAll 과 같은 메서드가 모두 준비 되어 있는 것이 눈에 띕니다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
</head>
<body>
<h1>Hello world</h1>
<pre th:text="${data}"></pre>
</body>
</html>
repositoryTest.html 파일도 간단하게 작성 했습니다.
서버를 켜고 매핑된 url로 접속을 해 보겠습니다.
서버가 구동되는데 전혀 문제가 없습니다. 일단 해당 테이블이 비어있다보니 빈 배열만 표시되는 것을 확인 할 수 있습니다.
5. Entity 의 CRUD 'C', 'R'
일단 간단하게라도 입력을 할 수 있도록 구조를 변경해야 합니다.
html 파일을 먼저 손봅니다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
</head>
<body>
<h1>I am CRUD machine</h1>
<form method="post" action="/repository" th:object="${formModel}">
<input type="text" name="id" placeholder="id" th:value="*{id}"/>
<input type="text" name="name" placeholder="name" th:value="*{name}"/>
<input type="text" name="memo" placeholder="memo" th:value="*{memo}"/>
<input type="submit"/>
</form>
<pre th:text="${data}"></pre>
</body>
</html>
대충이나마 input 을 할 수 있는 구조를 만들어 줬습니다.
이번엔 컨트롤러 입니다.
package com.shane.boot.controller;
import javax.transaction.Transactional;
import com.shane.boot.Member;
import com.shane.boot.repositories.MyRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class RepositoryTest {
@Autowired
MyRepository repository;
@GetMapping("repository")
public ModelAndView index(
ModelAndView mav
,@ModelAttribute("formModel") Member member) {
mav.setViewName("repositoryTest");
Iterable<Member> list = repository.findAll();
mav.addObject("data", list);
return mav;
}
@PostMapping("repository")
@Transactional
public ModelAndView form(
@ModelAttribute("formModel") Member member
,ModelAndView mav){
repository.saveAndFlush(member);
return new ModelAndView("redirect:/repository");
}
}
Get과 Post Mapping을 각각 담당할 메서드들을 만들어 주었습니다.
Get 쪽은 변화가 거의 없지만, Post 쪽이 새로 추가 되었습니다.
Member 객체를 ModelAttribute로 받아서, repository에 추가하도록 구현 되었습니다.
이제 서버를 켜고 해당 url로 접속을 해 보았습니다. 입력 받을 준비가 되었습니다.
위와 같이 데이터를 입력 하고, submit을 누르면?
바로 아래에 출력이 됩니다. 하지만 toString이 없는 객체를 무작정 데려왔다 보니 , 일반적인 Object의 toString 형태로 출력되었습니다.
그래서 제대로 된 출력을 위해 html 파일을 조금 손 보았습니다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
</head>
<body>
<h1>I am CRUD machine</h1>
<form method="post" action="/repository" th:object="${formModel}">
<input type="text" name="id" placeholder="id" th:value="*{id}"/>
<input type="text" name="name" placeholder="name" th:value="*{name}"/>
<input type="text" name="memo" placeholder="memo" th:value="*{memo}"/>
<input type="submit"/>
</form>
<pre th:each="obj : ${data}">
<p th:text="|id : ${obj.id}, name: ${obj.name} memo:${obj.memo}|"></p>
</pre>
</body>
</html>
pre 에서 data 라는 이름으로 받은 리스트를 반복문을 돌리며 p 태그로 계속 출력 해 주도록 수정 했습니다.
다시 한번 제출 해 보았습니다.
이제 잘 기록이 됩니다 !
몇번을 더 입력 해 보았습니다. p 태그간의 간격이 아주 시원 시원 합니다. 입력한 값들이 모두 잘 저장되는 것을 확인 하실 수 있습니다.
HSQLDB는 기본적으로 메모리 내에 캐싱을 하기 때문에 서버를 껐다 켤 때마다 모든 데이터 베이스가 초기화 됩니다. 매번 새로운 값을 입력하기 번거롭기 때문에 위의 입력 내용들을 처음에 초기값으로 입력 하도록 코드를 일단 수정하도록 하겠습니다.
@PostConstruct
public void init(){
Member member1 = new Member();
member1.setId(1);
member1.setName("shane");
member1.setMemo("first member");
repository.saveAndFlush(member1);
Member member2 = new Member();
member2.setId(2);
member2.setName("jenny");
member2.setMemo("second member");
repository.saveAndFlush(member2);
Member member3 = new Member();
member3.setId(3);
member3.setName("jane");
member3.setMemo("third member");
repository.saveAndFlush(member3);
}
위의 PostContruct 내용을 RepositoryTest 클래스에 추가 해 두었습니다.
그럼 이제 서버를 껐다 켜도 3개의 정보를 가지게 됩니다.
서버를 재시작 했지만, 여전히 세 명의 멤버 DB를 가지고 있는 모습입니다.
6. Entity 의 CRUD 'U'
이번에는 업데이트를 해보도록 하겠습니다.
update가 가능하도록 html 파일을 먼저 만들어 줍니다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
</head>
<body>
<p>I am CR<b>U</b>D machine</p>
<form method="post" action="/repository" th:object="${formModel}">
<input type="hidden" name="id" placeholder="id" th:value="*{id}"/>
<input type="text" name="name" placeholder="name" th:value="*{name}"/>
<input type="text" name="memo" placeholder="memo" th:value="*{memo}"/>
<input type="submit"/>
</form>
<pre th:each="obj : ${data}">
<p th:text="|id : ${obj.id}, name: ${obj.name} memo:${obj.memo}|"></p>
</pre>
</body>
</html>
위에서 만들었던 html과 거의 차이가 없는데요, 차이가 있다면 id를 받는 input 태그를 hidden 으로 바꿨습니다.
그리고 MyRepository에, id를 바탕으로 Member를 한명 select 할 수 있는 메서드를 하나 추가합니다.
package com.shane.boot.repositories;
import com.shane.boot.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface MyRepository extends JpaRepository<Member, Long>{
public Member findById(long id);
}
findById 이름은 변경하면 안됩니다. 제가 selectMemberById 등으로 맘대로 바꿔봤는데 이게 정해진 이름으로 만들지 않으면 에러가 발생했습니다. 딱히 findById가 JpaRepository에 등록되어 overWriting 하는 것도 아닌데 신기합니다.
사실 메서드를 선언만 하고, 따로 구현하는 작업은 하지 않았기 때문에 맘대로 이름 짓는게 말도 안되긴 했습니다. 어쩄든 이렇게 findById 라는 메서드만 선언을 해 두면 알아서 메서드가 구현됩니다.
이러한 자동 생성이 가능한 이유는 JpaRepository에 '사전을 이용한 코드 자동 생성' 기능이 내장되어 있기 때문이라고 합니다.
findById라는 이름을 자동으로 쪼개어 find / by / id 라는 세개의 인수로 나눈 후에, 해당 이름을 바탕으로
"from member where id= ? " 라는 쿼리를 자동으로 생성 해 줍니다. 해당 내용은 추후에 많이 파고 들어야 할 것 같습니다.
마지막으로 컨트롤러도 하나 만들어줍니다.
package com.shane.boot.controller;
import javax.transaction.Transactional;
import com.shane.boot.Member;
import com.shane.boot.repositories.MyRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class EditTest {
@Autowired
MyRepository repository;
@GetMapping("/edit/{id}")
public ModelAndView editForm(
ModelAndView mav
,@ModelAttribute("formModel") Member member
,@PathVariable long id) {
mav.setViewName("editTest");
member = repository.findById(id);
mav.addObject("formModel", member);
return mav;
}
@PostMapping("/edit")
@Transactional
public ModelAndView update(
@ModelAttribute("formModel") Member member
,ModelAndView mav){
repository.saveAndFlush(member);
return new ModelAndView("redirect:/repository");
}
}
이번에도 Get, Post 따로 메서드를 생성하는데요. 특이한건 Update 할 때에도 insert와 마찬가지로 saveAndFlush를 사용합니다.
edit/1 로 get 요청을 보내니, 저장되어 있는 데이터를 받아와서 보여줍니다.
이렇게 edited 로 수정해서 Submit을 해보면,
바로 수정된 내용을 확인 할 수 있습니다. C/R/U 까지 별 탈 없이 성공했습니다.
7. Entity 의 CRUD 'D'
마지막으로 삭제 입니다. CRUD는 새로 배울때마다 정말 재밌는데, 그 여정이 만만치는 않습니다.
삭제는 간단하기 때문에 책에서는 또 템플릿을 생성해서 했지만, 저는 기존의 템플릿을 재활용 하도록 하겠습니다.
edit.html 을 수정합니다
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
</head>
<body>
<p>I am CR<b>U</b>D machine</p>
<form method="post" action="/repository" th:object="${formModel}">
<input type="hidden" name="id" placeholder="id" th:value="*{id}"/>
<input type="text" name="name" placeholder="name" th:value="*{name}"/>
<input type="text" name="memo" placeholder="memo" th:value="*{memo}"/>
<input type="submit"/>
</form>
<form method="post" action="/delete">
<input type="hidden" name="id" placeholder="id" th:value="*{id}"/>
<input type="submit" value="delete">
</form>
<pre th:each="obj : ${data}">
<p th:text="|id : ${obj.id}, name: ${obj.name} memo:${obj.memo}|"></p>
</pre>
</body>
</html>
form 하나 더 만들어서 delete 라는 주소로 post 요청을 하도록 했습니다. hidden 으로 id를 보내도록 해두었고, delete라는 버튼도 하나 생성 했습니다.
보기 썩 좋진 않지만, 간단하게 삭제 버튼을 추가 했습니다.
이제 delete 를 처리할 method를 만들어야 합니다.
이것도 따로 class를 새로 생성하지 않고, EditTest 라는 이름으로 만들어두었던 컨트롤러에 메서드만 추가하겠습니다.
@PostMapping("/delete")
@Transactional
public ModelAndView remove(@RequestParam long id){
repository.deleteById(id);
return new ModelAndView("redirect:/repository");
}
deleteById 로 해당 내용을 삭제 하고, repository 페이지로 돌아가게끔 구현했습니다.
이제 서버를 실행 해서 정말 제거가 되는지 확인 해 보도록 하겠습니다.
delete를 누르면?
jane 이 깔끔하게 삭제되어, shane 과 jenny 만 남아 있는 것을 확인 할 수 있습니다.
이렇게 C/R/U/D 를 모두 완료했습니다.
위에서 입력했던 모든 코드는 아래에서 확인 하실 수 있습니다. 수고하셨습니다.
https://github.com/Shane-Park/springBootStudy