package com.example.sbb;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Question,Integer> {
}
JpaRepository : JPA 가 제공하는 인터페이스 중 하나로 CRUD 작업을 처리하는 메서드들을 이미 내장하고 있어 데이터 관리 작업을 좀 더 편리하게 처리 가능JpaRepository<Question,Integer> : Question 엔티티로 리포지터리 생성, Question 엔티티의 기본키가 Integerpackage com.example.sbb;
import org.springframework.data.jpa.repository.JpaRepository;
public interface AnswerRepository extends JpaRepository<Answer,Integer> {
}
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testRuntimeOnly : 해당 라이브러리가 테스트 실행 시에만 사용package com.example.sbb;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
// q1, q2라는 질문 엔티티의 객체를 생성하고 QuestionRepository를 이용하여 그 값을 DB 에 저장
Question q1 = new Question();
q1.setSubject("sbb가 무엇인가요?");
q1.setContent("sbb에 대해서 알고 싶습니다.");
q1.setCreateDate((LocalDateTime.now()));
this.questionRepository.save(q1);
Question q2 = new Question();
q2.setSubject("스프링부트 모델 질문입니다.");
q2.setContent("id는 자동으로 생성되나요?");
q2.setCreateDate(LocalDateTime.now());
this.questionRepository.save(q2);
}
}
@SpringBootTest : SbbApplicationTests 클래스가 스프링부트의 테스트 클래스라는 것을 알려주는 것@Autowired : 의존성 주입 기능 ( QuestionRepository 객체 주입)@Test : test.Jpa 메서드가 테스트 메서드임을 선언
@Test
void testJpa() {
List<Question> all = this.questionRepository.findAll();
assertEquals(2, all.size());
Question q = all.get(0);
assertEquals("sbb가 무엇인가요?", q.getSubject());
}
assertEquals(기댓값,실젯값) : 기댓값과 실제값이 동일한지 확인 @Test
void testJpa() {
// questionRepository 를 사용하여 DB에서 id가 1인 질문을 조회
//리턴 타입은 Optional : findById로 호출한 값이 존재할 수도 있고 없을 수도 있기 때문에 사용
Optional<Question> oq = this.questionRepository.findById(1);
//만약 값이 존재한다고 확인하면, get()을 통해 Question 객체의 값을 얻는다
if(oq.isPresent()) {
Question q = oq.get();
assertEquals("sbb가 무엇인가요?", q.getSubject());
}
}
package com.example.sbb;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Question,Integer> {
Question findBySubject(String subject);
}
@Test
void testJpa() {
Question q = this.questionRepository.findBySubject("sbb가 무엇인가요?");
assertEquals(1, q.getId());
}
findBy+엔티티의 속성명 : 입력한 속성의 값으로 데이터 조회 가능package com.example.sbb;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Question,Integer> {
Question findBySubject(String subject);
Question findBySubjectAndContent(String subject, String content);
}
@Test
void testJpa() {
Question q = this.questionRepository.findBySubjectAndContent(
"sbb가 무엇인가요?", "sbb에 대해서 알고 싶습니다.");
assertEquals(1, q.getId());
}
package com.example.sbb;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface QuestionRepository extends JpaRepository<Question,Integer> {
Question findBySubject(String subject);
Question findBySubjectAndContent(String subject, String content);
List<Question> findBySubjectLike(String subject);
}
@Test
void testJpa() {
List<Question> qList = this.questionRepository.findBySubjectLike("sbb%");
Question q = qList.get(0);
assertEquals("sbb가 무엇인가요?", q.getSubject());
}
sbb% : sbb로 시작하는 것들이 찾아진다@Test
void testJpa() {
Optional<Question> oq = this.questionRepository.findById(1);
assertTrue(oq.isPresent());
Question q = oq.get();
// 질문 데이터 엔티티 데이터 조회 후, subject 속성을 '수정된 제목' 으로 수정
q.setSubject("수정된 제목");
// 변경된 질문을 db에 저장
this.questionRepository.save(q);
}
-assertTrue() :괄호 안의 값이 true 인지 테스트
oq.isPresent() : false 리턴하면 오류 발생, 테스트 종료 @Test
void testJpa() {
assertEquals(2, this.questionRepository.count());
Optional<Question> oq = this.questionRepository.findById(1);
assertTrue(oq.isPresent());
Question q = oq.get();
this.questionRepository.delete(q);
assertEquals(1, this.questionRepository.count());
}
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Autowired
private AnswerRepository answerRepository;
@Test
void testJpa() {
// ID 가 2인 질문 데이터를 가져와 답변의 question 속성에 대입해 답변 데이터 생성
Optional<Question> oq = this.questionRepository.findById(2);
assertTrue(oq.isPresent());
Question q = oq.get();
Answer a = new Answer();
a.setContent("네 자동으로 생성됩니다.");
a.setQuestion(q); // 어떤 질문의 답변인지 알기위해서 Question 객체가 필요하다.
a.setCreateDate(LocalDateTime.now());
this.answerRepository.save(a);
}
@Test
void testJpa() {
Optional<Answer> oa = this.answerRepository.findById(1);
assertTrue(oa.isPresent());
Answer a = oa.get();
assertEquals(2, a.getQuestion().getId());
}
a.getQuestion() : a 는 답변 객체, a.getQuestion() 은 답변에 연결된 질문 객체 @Transactional : 메서드 종료시까지 DB 세션이 유지비슷한 기능이나 관련된 개념을 함께 묶어 코드를 구조화하여 정리 → 코드를 쓰거나 읽을 때 혹은 유지 보수 할 때 편리
1) 패키지 생성

2) 생성된 패키지에 정리


package com.example.sbb.question;
import lombok.Getter;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class QuestionController {
@GetMapping("/question/list")
@ResponseBody
public String list(){
return "quetion list";
}
}

implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
1) 템플릿에 html(question_list) 파일 생성

2) html 파일 작성

3) 컨트롤러 수정
package com.example.sbb.question;
import lombok.Getter;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class QuestionController {
@GetMapping("/question/list")
// 템플릿을 받을 것이기 때문에 ResponseBody는 삭제해준다
// @ResponseBody
public String list(){
return "question_list";
}
}
4) 브라우저 재실행

package com.example.sbb.question;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
@RequiredArgsConstructor
@Controller
public class QuestionController {
// @RequiredArgsConstructor 를 사용하여 QuestionController를 생성할 때
// 롬복으로 만들어진 생성자에 의해 questionRepository 객체가 자동으로 주입
private final QuestionRepository questionRepository;
@GetMapping("/question/list")
public String list(Model model){
//findAll() 에 의해 질문 목록 db인 questionList 생성 Model 객체에 'questionList' 라는 이름으로 저장
List<Question> questionList=this.questionRepository.findAll();
model.addAttribute("questionList",questionList);
return "question_list";
}
}
-@RequiredArgsConstructor : final이 붙은 속성을 포함하는 생성자를 자동으로 만들어주는 역할
Model : 자바 클래스와 템플릿 간의 연결 고리 역할<table>
<thead>
<tr>
<th>제목</th>
<th>작성일시</th>
</tr>
</thead>
<tbody>
<tr th:each="question : ${questionList}">
<td th:text="${question.subject}"></td>
<td th:text="${question.createDate}"></td>
</tr>
</tbody>
</table>
th:each="question : ${questionList}"th: : 타임리프에서 사용하는 속성임을 알려준다, 자바 코드와 연결${questionList} : Model 객체에 저장한 questionList를 읽을 수 있음
th:if="${question != null}"
-th:each : 반복문
1) th:each="question:${questionList}"
2) th:each="qeustion,loop:${questionList}"
-th:text : 텍스트값 출력
th:text=${question.subject}"
<tr th:each="question : ${questionList}">
<td>[[${question.subject}]]</td>
<td>[[${question.createDate}]]</td>
</tr>

package com.example.sbb;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class MainController {
@GetMapping("/sbb")
@ResponseBody
public String index() {
return "안녕하세요 sbb에 오신것을 환영합니다.";
}
// root 메서드 추가하고 / URL 매핑
@GetMapping("/")
public String root(){
return "redirect:/question/list";
}
}
redirect:/question/list : /question/list URL로 페이지를 리다이렉트하라는 명령어package com.example.sbb.question;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@RequiredArgsConstructor
@Service
public class QuestionService {
private final QuestionRepository questionRepository;
//컨트롤러에서 리포지터리를 사용했던 부분을 그대로 옮김
public List<Question> getList(){
return this.questionRepository.findAll();
}
}
@Service : 생성한 클래스를 서비스로 만들어주는 역할package com.example.sbb.question;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
@RequiredArgsConstructor
@Controller
public class QuestionController {
//레파지토리 > 서비스를 사용하도록 변경해준다
private final QuestionService questionService;
@GetMapping("/question/list")
public String list(Model model){
List<Question> questionList=this.questionService.getList();
model.addAttribute("questionList",questionList);
return "question_list";
}
}
<table>
<thead>
<tr>
<th>제목</th>
<th>작성일시</th>
</tr>
</thead>
<tbody>
<tr th:each="question : ${questionList}">
<td>
<a th:href="@{|/question/detail/${question.id}|}" th:text="${question.subject}"></a>
</td>
<td th:text="${question.createDate}"></td>
</tr>
</tbody>
</table>
<td> : 질문 목록의 제목을 텍스트로 출력하던 것에서 질문의 상세 내용이 담긴 웹 페이지로 이동할 수 있는 링크로 변경
<a th:href="@{|/question/detail/${question.id}|}" th:text="${question.subject}">th:href : URL 연결@{} : 반드시 사이에 URL 넣어줘야 한다/question/detail/${question.id} : question/detail 주소 후에 id는 변경되기 때문에 변수로 넣어준다|| : /question/detail/ 문자열과 ${question.id} 객체 값을 더해줄 때 반드시 좌우로 감싸줘야 한다th:text="${question.subject}" : 보여질 출력 값package com.example.sbb.question;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
@RequiredArgsConstructor
@Controller
public class QuestionController {
private final QuestionService questionService;
@GetMapping("/question/list")
public String list(Model model){
List<Question> questionList=this.questionService.getList();
model.addAttribute("questionList",questionList);
return "question_list";
}
@GetMapping(value = "/question/detail/{id}")
public String detail(Model model, @PathVariable("id") Integer id) {
return "question_detail";
}
}
@PathVariable : 변하는 값을 얻을 때 사용<h1>제목</h1>
<div>내용</div>

package com.example.sbb.question;
import com.example.sbb.DataNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@RequiredArgsConstructor
@Service
public class QuestionService {
private final QuestionRepository questionRepository;
public List<Question> getList(){
return this.questionRepository.findAll();
}
public Question getQuestion(Integer id){
Optional<Question> question = this.questionRepository.findById(id);
// isPresent로 id 값이 존재하는지 검사하는 과정
// 만약 id 값이 없다면 예외 클래스가 실행된다
if(question.isPresent()){
return question.get();
} else{
throw new DataNotFoundException("question not found");
}
}
}
// 예외처리 클래스를 만들어준다
package com.example.sbb;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value= HttpStatus.NOT_FOUND,reason = "entity not found")
public class DataNotFoundException extends RuntimeException{
private static final long serialVersionUID = 1L;
public DataNotFoundException(String message){
super(message);
}
}
package com.example.sbb.question;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
@RequiredArgsConstructor
@Controller
public class QuestionController {
private final QuestionService questionService;
@GetMapping("/question/list")
public String list(Model model){
List<Question> questionList=this.questionService.getList();
model.addAttribute("questionList",questionList);
return "question_list";
}
@GetMapping(value = "/question/detail/{id}")
public String detail(Model model, @PathVariable("id") Integer id) {
Question question = this.questionService.getQuestion(id);
model.addAttribute("question",question);
return "question_detail";
}
}
<h1 th:text="${question.subject}"></h1>
<div th:text="${question.content}"></div>
Model 객체에서 question 이름으로 객체를 저장했으므로 사용 가능

없는 데이터를 조회할 경우 예외 클래스가 실행되어 오류 발생

package com.example.sbb.question;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.List;
// URL 매핑은 항상 /question 프리픽스로 시작하도록 설정
@RequestMapping("/question")
@RequiredArgsConstructor
@Controller
public class QuestionController {
private final QuestionService questionService;
///question 부분 삭제
@GetMapping("/list")
public String list(Model model){
List<Question> questionList=this.questionService.getList();
model.addAttribute("questionList",questionList);
return "question_list";
}
///question 부분 삭제
@GetMapping(value = "/detail/{id}")
public String detail(Model model, @PathVariable("id") Integer id) {
Question question = this.questionService.getQuestion(id);
model.addAttribute("question",question);
return "question_detail";
}
}