✔️"점프 투 스프링 부트" 교재 2-01부터 2-04까지를 복습한 후에, 2-05까지 학습
우리가 최종적으로 구현 해야할 것은 질문 답변 게시판으로, 데이터를 저장하거나 조회하거나 수정하는 등의 기능을 구현해야 한다.
JPA를 사용하기 전에 데이터를 저장할 데이터베이스를 설치한다.
💡 H2 데이터베이스
H2 데이터베이스는 주로 개발용이나 소규모 프로젝트에서 사용되는 파일 기반의 경량 데이터베이스이다. 개발시에는 H2를 사용하여 빠르게 개발하고 실제 운영시스템은 좀 더 규모있는 DB를 사용하는 것이 일반적인 개발 패턴이다.
💡 H2 데이터베이스 설치
(... 생략 ...)
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
}
(... 생략 ...)
# DATABASE
spring.h2.console.enabled=true //H2 콘솔의 접속을 허용할지의 여부이다. true로 설정한다.
spring.h2.console.path=/h2-console //콘솔 접속을 위한 URL 경로이다.
spring.datasource.url=jdbc:h2:~/local //데이터베이스 접속을 위한 경로이다.
spring.datasource.driverClassName=org.h2.Driver //데이터베이스 접속시 사용하는 드라이버이다.
spring.datasource.username=sa //데이터베이스의 사용자명이다. (사용자명은 기본 값인 sa로 설정한다.)
spring.datasource.password= //데이터베이스의 사용자명이다. (사용자명은 기본 값인 sa로 설정한다.)
spring.datasource.url
에 설정한 경로에 해당하는 데이터베이스 파일을 생성한다. 위에서 spring.datasource.url
을 jdbc:h2:~/local
로 설정했기 때문에 사용자의 홈디렉터리(~
에 해당하는 경로) 밑에 local.mv.db
라는 파일을 생성⭐️ window에서 local.mv.db 파일 생성하기
- copy con local.mv.db
이제 자바 프로그램에서 H2 데이터베이스를 사용할 수 있게 해야함. 자바 프로그램에서 데이터베이스에 데이터를 저장하거나 조회하려면 JPA를 사용해야함
(... 생략 ...)
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
}
(... 생략 ...)
# JPA
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect //데이터베이스 엔진 종류를 설정한다.
spring.jpa.hibernate.ddl-auto=update
//데이터베이스 엔진 종류를 설정한다.
// 개발 환경에서는 보통 update 모드를 사용하고, 운영환경에서는 none 또는 validate 모드를 사용한다.
이제 질문답변 게시판에서 사용할 질문과 답변에 해당하는 엔티티를 만들어보자.
package com.gdsc.webboard.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.List;
@Getter
@Setter
@Entity //해당 어노테이션을 적용해야 JPA가 엔티티로 인식
public class Question {
@Id //DB에서 id를 뜻함
@GeneratedValue(strategy = GenerationType.IDENTITY) //id를 어떤 방식으로 생성할건지
private Integer id;
@Column(length=200) //최대 길이가 200으로 지정 , 속성 사용 안하면 Column 어노테이션 사용하지 않음
private String subject;
@Column(columnDefinition = "TEXT")
private String content;
private LocalDateTime createDate;
@OneToMany(mappedBy = "question", cascade = CascadeType.REMOVE) //질문에서 대답으로 One to Many
private List<Answer> answerList; //여러개의 답변이니까 list
}
1. @Entity
@Entity
애너테이션을 적용2. @Getter, @Setter
3. @Id
4. @GeneratedValue
strategy
는 고유번호를 생성하는 옵션으로 GenerationType.IDENTITY
는 해당 컬럼만의 독립적인 시퀀스를 생성하여 번호를 증가시킬 때 사용5. @Column
columnDefinition = "TEXT"
은 "내용"처럼 글자 수를 제한할 수 없는 경우에 사용6. @OneToMany
cascade = CascadeType.REMOVE
를 사용경로: src\main\java\com\gdsc\webboard\entity\Answer.java
package com.gdsc.webboard.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
@Getter
@Setter
@Entity
public class Answer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(columnDefinition = "TEXT")
private String content;
private LocalDateTime createDate;
@ManyToOne //Many-답변 One-질문 //답변에서 질문으로 -> Many to One
private Question question;
}
엔티티 생성 후, H2 콘솔에 접속하면 Question과 Answer 테이블이 자동으로 생성되어 있음
1. QuestionRepository.java
경로: src\main\java\com\gdsc\webboard\repository\QuestionRepository.java
package com.gdsc.webboard.repository;
import com.gdsc.webboard.entity.Question;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Question, Integer> {
}
2. AnswerRepository.java
경로: src\main\java\com\gdsc\webboard\repository\AnswerRepository.java
package com.gdsc.webboard.repository;
import com.gdsc.webboard.entity.Answer;
import org.springframework.data.jpa.repository.JpaRepository;
public interface AnswerRepository extends JpaRepository<Answer, Integer> {
}
<Question, Integer>
처럼 리포지터리의 대상이 되는 엔티티의 타입(Question)과 해당 엔티티의 PK의 속성 타입(Integer)을 지정해야 함1. 먼저, 테스트 파일을 생성
경로: src\test\java\com\gdsc\webboard\SbbApplicationTests.java
package com.gdsc.webboard;
import com.gdsc.webboard.entity.Question;
import com.gdsc.webboard.repository.QuestionRepository;
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
public class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
Question q1 = new Question(); //q1 엔티티 객체 생성
q1.setSubject("sbb가 무엇인가요?");
q1.setContent("sbb에 대해서 알고 싶습니다.");
q1.setCreateDate(LocalDateTime.now());
this.questionRepository.save(q1); // 첫번째 질문 저장
Question q2 = new Question();//q2 엔티티 객체 생성
q2.setSubject("스프링부트 모델 질문입니다.");
q2.setContent("id는 자동으로 생성되나요?");
q2.setCreateDate(LocalDateTime.now());
this.questionRepository.save(q2); // 두번째 질문 저장
}
}
@SpringBootTest: SbbApplicationTests 클래스가 스프링부트 테스트 클래스임을 의미
@Autowired: 스프링의 DI 기능으로 questionRepository 객체를 스프링이 자동으로 생성해 준다.
📌 Junit assertEquals 메서드
- assertEquals는
assertEquals(기대값, 실제값)
와 같이 사용하고 기대값과 실제값이 동일한지를 조사- 만약 기대값과 실제값이 동일하지 않다면 테스트는 실패로 처리
1. findAll
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
List<Question> all = this.questionRepository.findAll();
assertEquals(2, all.size());
Question q = all.get(0);
assertEquals("sbb가 무엇인가요?", q.getSubject());
}
}
2. findById
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
Optional<Question> oq = this.questionRepository.findById(1);
if(oq.isPresent()) {
Question q = oq.get();
assertEquals("sbb가 무엇인가요?", q.getSubject());
}
}
}
3. findBySubject
package com.gdsc.webboard.repository;
import com.gdsc.webboard.entity.Question;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Question, Integer> {
Question findBySubject(String subject);
}
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
Question q = this.questionRepository.findBySubject("sbb가 무엇인가요?");
assertEquals(1, q.getId());
}
}
📌 인터페이스에 findBySubject라는 메서드를 선언만 하고 구현은 하지 않았는데 어떻게 실행이 되는 것인가?
→ JpaRepository를 상속한 QuestionRepository 객체가 생성되고, 메서드가 실행될 때 JPA가 해당 메서드명을 분석하여 쿼리를 만들고 실행한다.
→ 즉,findBy+엔티티의 속성명
과 같은 리포지토리 메서드를 작성하면 해당 속성의 값으로 데이터를 조회할 수 있다.
📌 실행되는 쿼리 로그에서 보는 방법
- application.properties 파일에 해당 코드 추가
# JPA spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect spring.jpa.hibernate.ddl-auto=update spring.jpa.properties.hibernate.format_sql=true spring.jpa.properties.hibernate.show_sql=true
4. findBySubjectAndContent
package com.gdsc.webboard.repository;
import com.gdsc.webboard.entity.Question;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Question, Integer> {
Question findBySubject(String subject);
Question findBySubjectAndContent(String subject, String content);
}
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
Question q = this.questionRepository.findBySubjectAndContent(
"sbb가 무엇인가요?", "sbb에 대해서 알고 싶습니다.");
assertEquals(1, q.getId());
}
}
5. findBySubjectLike
package com.gdsc.webboard.repository;
import com.gdsc.webboard.entity.Question;
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);
}
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
List<Question> qList = this.questionRepository.findBySubjectLike("sbb%");
Question q = qList.get(0);
assertEquals("sbb가 무엇인가요?", q.getSubject());
}
}
sbb%
: "sbb"로 시작하는 문자열%sbb
: "sbb"로 끝나는 문자열%sbb%
: "sbb"를 포함하는 문자열package com.gdsc.webboard;
import com.gdsc.webboard.entity.Question;
import com.gdsc.webboard.repository.QuestionRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Optional;
@SpringBootTest
public class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
Optional<Question> oq = this.questionRepository.findById(1);
assertTrue(oq.isPresent()); //값이 true인지 테스트
Question q = oq.get(); //질문데이터가 있으면, 질문데이터를 얻어 q 객체에 저장
q.setSubject("수정된 제목"); //subject를 "수정된 제목"으로 변경
this.questionRepository.save(q); //변경된 Question 데이터를 save 메서드를 사용해 저장
}
}
📌 Junit의 Assertion 클래스
모든 Assertion은 Assert 클래스가 있음
public class Assert extends java.lang.Object
이 클래스는 테스트 작성에 유용한 Assertion 메서드 집합을 제공하고, 실패한 Assertion 만 기록된다. Assert는 프로그램에 대한 가정을 테스트할 수 있는 것으로, 프로그램의 오류를 감지하고 수정하는 효과적인 방법을 제공함.
package com.gdsc.webboard;
import com.gdsc.webboard.entity.Question;
import com.gdsc.webboard.repository.QuestionRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SpringBootTest
public class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void delete(){
assertEquals(2, this.questionRepository.count()); //삭제 전 데이터 건수가 2인지 테스트
Optional<Question> oq = this.questionRepository.findById(1); //findById 메서드로 데이터를 찾음
assertTrue(oq.isPresent()); //데이터가 있는지 테스트
Question q = oq.get(); //데이터가 있으면 q 객체에 대입
this.questionRepository.delete(q); //delete 메서드를 사용하여 데이터를 삭제
assertEquals(1, this.questionRepository.count()); //삭제 후 데이터 건수가 1인지 테스트
}
@SpringBootTest
public class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Autowired
private AnswerRepository answerRepository; //AnswerRepository 객체를 @Autowired로 주입
@Test
void 답변저장(){
//답변 데이터를 생성하려면 질문 데이터가 필요
Optional<Question> oq = this.questionRepository.findById(2); //id가 2인 질문데이터 가져옴
assertTrue(oq.isPresent()); //데이터가 있는지 확인
Question q = oq.get(); //있으면, q 객체에 저장
Answer a = new Answer(); //Answer 객체 생성
a.setContent("네 자동으로 생성됩니다."); //데이터 세팅
a.setQuestion(q);//데이터 세팅
a.setCreateDate(LocalDateTime.now());//데이터 세팅
this.answerRepository.save(a); //save 메서드를 통한 데이터 저장
}
@Test
void 답변조회(){
Optional<Answer> oa = this.answerRepository.findById(1); //id가 1인 answer데이터 조회
assertTrue(oa.isPresent()); //데이터가 있는지 확인
Answer a = oa.get(); //데이터가 있으면 a 객체에 저장
assertEquals(2, a.getQuestion().getId()); //질문의 id가 2인지 확인
}
답변에 연결된 질문 찾기는 Answer 엔티티에 question 속성이 정의되어 있어서 매우 쉽다. 반대의 경우(질문에서 답변을 찾는 경우), 질문 엔티티에 정의한 answerList를 사용하면 쉽게 구할 수 있다.
@Test
void 질문에서답변찾기(){
Optional<Question> oq = this.questionRepository.findById(2); //id가 2인 question을 조회
assertTrue(oq.isPresent()); //데이터가 있는지 확인
Question q = oq.get(); //q 객체에 저장
List<Answer> answerList = q.getAnswerList(); //q 객체에서 answer list 조회
assertEquals(1, answerList.size()); //answer list 데이터 개수 확인
assertEquals("네 자동으로 생성됩니다.", answerList.get(0).getContent()); //첫번째 데이터 내용 확인
}
사실 이 문제는 테스트 코드에서만 발생함 → 실제 서버에서 JPA 프로그램을 실행할 때는 DB 세션이 종료되지 않기 때문
@Transactional
@Test
void 질문에서답변찾기(){
Optional<Question> oq = this.questionRepository.findById(2); //id가 2인 question을 조회
assertTrue(oq.isPresent()); //데이터가 있는지 확인
Question q = oq.get(); //q 객체에 저장
List<Answer> answerList = q.getAnswerList(); //q 객체에서 answer list 조회
assertEquals(1, answerList.size()); //answer list 데이터 개수 확인
assertEquals("네 자동으로 생성됩니다.", answerList.get(0).getContent()); //첫번째 데이터 내용 확인
}