1. 리포지터리로 데이터베이스 관리하기
1) 리포지터리 생성
JPA 리포지터리
- 데이터베이스 테이블의 데이터들을 저장, 조회, 수정, 삭제 등을 할 수 있도록 도와주는 인터페이스
- 테이블에 접근하고, 데이터를 관리하는 메서드를 제공
- CRUD 작업을 처리하는 메서드들을 이미 내장하고 있어 데이터 관리 작업을 좀 더 편리하게 처리할 수 있음
QuestionRepository.java
package com.example.demo;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Question,Integer> {
}
- 생성한 QuestionRepository 인터페이스를 리포지터리로 만들기 위해 JpaRepository 인터페이스를 상속
- JpaRepository<Question,Integer>
-> Question 엔티티로 리포지터리를 생성
-> Question 엔티티의 기본키가 Integer
2) JUnit 설치
JUnit
- 테스트 코드를 작성하고, 작성한 테스트 코드를 실행할 때 사용하는 자바의 테스트 프레임워크
- 작성한 리포지터리가 정상적으로 동작하는지 테스트
build.gradle
3) 질문 데이터 저장
-질문 엔티티로 만든 테이블에 데이터를 생성하고 저장
SbbApplicationTests.java
package com.example.demo;
import java.time.LocalDateTime;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
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);
}
}
@SpringBoodTest
- SbbApplicationTests 클래스가 스프링 부트의 테스트 클래스임을 의미
@Autowired
- 질문 엔티티의 데이터를 생성할 때 리포지터리(QuestionRepository)가 필요
- @Autowired를 통해 스프링의 '의존성 주입(DI)'를 사용하여 QuestionRepository의 객체 주입
- 스프링 부트가 questionRepository 객체를 자동으로 만들어 주입
의존성 주입(DI, Dependency Injection)
- 스프링이 객체를 대신 생성하여 주입하는 기법
- @Autowired 사용 / Setter 메서드 사용 / 생성자 사용
@Test
- testJpa 메서드가 테스트 메서드임을 나타냄
- testJpa 메서드는 q1,q2라는 질문 엔티티의 객체를 생성하고 QustionRepository를 이용하여 그 값을 데이터베이스에 저장
ID | Content | CreateDate | Subject |
1 | sbb에 대해서 알고 싶습니다. | 2024-01-09-13:57:00 ... | sbb가 무엇인가요 |
2 | id는 자동으로 생성되나요? | 2024-01-09 -13:57:00.... | 스프링부트 모델 질문입니다. |
SbbApplicationTest 클래스 실행
- H2 콘솔에 접속하여 Select * From Question 쿼리문 실행
4) 질문 데이터 조회하기
findAll 메서드
- question 테이블에 저장된 모든 데이터를 조회하기 위해 사용
SbbApplicationTests.java
package com.example.demo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
List<Question> all = this.questionRepository.findAll();
assertEquals(2,all.size());
//첫 번째 데이터의 제목이 'sbb가 무엇인가요?' 데이터와 일치하는지 테스트
Question q = all.get(0);
assertEquals("sbb가 무엇인가요?", q.getSubject());
}
}
assertEquals 메서드
- assertEquals(기댓값, 실젯값)
- 기댓값과 실젯값이 동일하지 않다면 테스트는 실패로 처리
- 2개의 질문 데이터를 저장했기 때문에 데이터 사이즈는 2가 되어야 함
- Junit의 메소드로 테스트에서 예상한 결과와 실제 결과가 동일한지를 확인하는 목적으로 사용
- JPA 또는 데이터베이스에서 데이터를 올바르게 가져오는지를 확인
findById 메서드
- 질문 엔티티의 기본키인 id의 값을 활용해 데이터를 조회
sbbApplication.java
package com.example.demo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
//데이터베이스에서 id가 1인 질문 조회
Optional<Question> oq = this.questionRepository.findById(1);
if(oq.isPresent()) {
Question q = oq.get();
assertEquals("sbb가 무엇인가요?" ,q.getSubject());
}
}
}
- findById의 리턴 타입은 Question이 아닌 Optional
-> findById로 호출한 값이 존재할 수도 있고, 존재하지 않을 수도 있어서 리턴 타입으로 Optional이 사용됨
- isPresent()를 통해 값이 존재한다는 것을 확인했으면, get() 메서드를 통해서 실제 Question 객체의 값을 얻음
Optional
- 값을 처리하기 위한 (null값을 유연하게 처리하기 위한)클래스
- isPresent() 메서드로 값이 존재하는지 확인 가능
findBySubject 메서드
- 질문 엔티티의 subject 값으로 데이터를 조회
- 기본적으로 제공하는 메서드가 아님
- QuestionRepository 인터페이스에 findBySubject 메서드 선언
- JPA에 리포지터리의 메서드명을 분석하여 쿼리를 만들고 실행하는 기능이 있기 때문에 findBy + 엔티티의 속성명과 같은 리포지터리의 메서드를 작성하면 입력한 속성의 값으로 데이터를 조회 가능
QuestionRepository.java
package com.example.demo;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Question,Integer> {
Question findBySubject(String subject);
}
SbbApplicationTests.java
- subject 값으로 테이블에 저장된 데이터를 조회
package com.example.demo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
Question q = this.questionRepository.findBySubject("sbb가 무엇인가요?");
assertEquals(1,q.getId());
}
}
- findBySubject 메서드를 호출할 때 실제 데이터베이스에서는 어떤 쿼리문이 실행되는지 콘솔 로그에서 확인
application.properties
findBySubjectAndContent 메서드
- subject와 content를 함께 조회
- SQL을 활용해 데이터베이스에서 두 개의 열(엔티티의 속성)을 조회하기 위해서는 And 연산자 사용
QuestionRepository.java
- 리포지터리에 findBySubjectAndContent 메서드 추가
package com.example.demo;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Question,Integer> {
Question findBySubject(String subject);
Question findBySubejctAndContent(String subject, String content);
}
SbbApplicationTests.java
package com.example.demo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
Question q = this.questionRepository.findBySubejctAndContent("sbb가 무엇인가요?","sbb에 대해서 알고 싶습니다." );
assertEquals(1,q.getId());
}
}
- 리포지터리의 메서드명은 데이터를 조회하는 쿼리문의 where 조건을 결정하는 역할을 함
SQL의 연산자 | 리포지터리의 메서드 예 | 설명 |
And | findBySubjectAndContent(String subject, String content) | Subject, Content 열과 일치하는 데이터를 조회 |
Or | findBySubjectOrContent(String subject, String content) | Subject열 또는 Content 열과 일치하는 데이터 조회 |
Between | findByCreateDateBetween(LocalIDateTime fromDate, LocalDateTimetoDate) | CreateDate 열의 데이터 중 정해진 범위 내에 있는 데이터 조회 |
LessThan | findByIdLessThan(Integer id) | Id 열에서 조건보다 작은 데이터를 조회 |
GreaterThanEqual | findByGreaterThanEqual(Integer id) | Id 열에서 조건보다 크거나 같은 데이터를 조회 |
Like | findBySubjectLike(String subject) | Subject 열에서 문자열 'subject'와 같은 문자열을 포함한 데이터만 조회 |
In | findBySubjectIn(String[] subjects) | Subject 열의 데이터가 주어진 배열에 포함되는 데이터만 조회 |
OrderBy | findBySubjectOrderByCreateDateAsc(String subject) | Subject 열 중 조건에 일치하는 데이터를 조회하여 그 데이터를 반환할 때 CreateDate 열을 오름차순으로 정렬하여 반환 |
findBySubjectLike 메서드
- 질문 엔티티의 subject 열 값들 중에 특정 문자열을 포함하는 데이터를 조회
QuestionRepository.java
- subject열에서 특정 문자열을 포함하는 데이터를 찾기 위해 findBySubjectLike 메서드를 리포지토리에 추가
package com.example.demo;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Question,Integer> {
Question findBySubject(String subject);
Question findBySubejctAndContent(String subject, String content);
List<Question> findBySubjectLike(String subject);
}
SbbApplicationTests.java
package com.example.demo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@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());
}
}
5) 질문 데이터 수정
SbbApplicationTests.java
- 질문 엔티티의 데이터를 조회한 다음, subjec 속성을 '수정된 제목' 이라는 값으로 수정
- 변경된 질문을 데이터베이스에 저장하기 위해서 this.questionRepository.save(q)와 같이 리포지터리의 save 메서드 사용
package com.example.demo;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
Optional<Question> oq = this.questionRepository.findById(1);
assertTrue(oq.isPresent());
Question q = oq.get();
q.setSubject("수정된 제목");
this.questionRepository.save(q);
}
}
6) 질문 데이터 삭제
- 첫 번째 질문 삭제
- 리포지터리의 delete 메서드를 사용하여 데이터를 삭제
- 데이터 건수가 삭제하기 전에 2였는데, 삭제한 후 1이 되었는지를 테스트
- 리포지터리의 count 메서드는 테이블 행의 개수를 리턴
SbbApplicationTests.java
package com.example.demo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@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());
}
}
7) 답변 데이터 저장
- 답변 엔티티의 데이터를 생성하고 저장
- 답변 데이터를 저장할 때도 리포지터리(AnswerRepository)가 필요하므로 AnswerRepository의 객체를 @Autowired를 통해 주입
- 답변을 생성하려면 질문이 필요하므로 우선 질문을 조회해야 함
- questionRepository의 findById 메서드를 통해 id가 2인 질문 데이터를 가져와 답변의 question 속성에 대입해 답변 데이터를 생성
SbbApplicationTests.java
package com.example.demo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.LocalDateTime;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Autowired
private AnswerRepository answerRepository;
@Test
void testJpa() {
//question 열에 데이터를 생성하려면 질문 데이터를 조회해야 함
Optional<Question>oq = this.questionRepository.findById(2);
assertTrue(oq.isPresent());
Question q = oq.get();
Answer a = new Answer();
a.setContent("네 자동으로 생성됩니다.");
//답변 엔티티의 question 속성에 질문 데이터를 대입해 답변 데이터를 생성하려면
//이와 같이 Question 객체 q가 필요
a.setQuestion(q);
a.setCreateDate(LocalDateTime.now());
this.answerRepository.save(a);
}
}
8) 답변 데이터 조회
- id값이 1인 답변을 조회
- 조회한 답변과 연결된 질문의 id가 2인지도 조회
SbbApplicationTests.java
- 질문을 조회한 후 이 질문에 달린 답변 전체를 구하는 테스트 코드
package com.example.demo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Autowired
private AnswerRepository answerRepository;
@Test
void testJpa() {
Optional<Answer> oa = this.answerRepository.findById(1);
assertTrue(oa.isPresent());
Answer a = oa.get();
assertEquals(2,a.getQuestion().getId());
}
}
9) 질문 데이터를 통해 답변 데이터 찾기
- 질문 엔티티에 정의한 answerList를 사용
sbbApplicationTests.java
package com.example.demo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Autowired
private AnswerRepository answerRepository;
@Test
void testJpa() {
Optional<Question> oq = this.questionRepository.findById(2);
assertTrue(oq.isPresent());
Question q = oq.get();
List<Answer>answerList = q.getAnswerList();
assertEquals(1,answerList.size());
assertEquals("네 자동으로 생성됩니다.", answerList.get(0).getContent());
}
}
오류
- QuestionRepository가 findById메서드를 통해 Question 객체를 조회하고 나면 DB 세션이 끊어지기 때문
- 그 이후에 실행되는 q.getAnswerList() 메서드는 세션이 종료되어 오류 발생
- answerList는 앞서 q 객체를 조회할 떄가 아니라 q.getAnswerList() 메서드를 호출하는 시점에 가져오기 때문
- 실제 서버에서 JPA 프로그램들을 실행할 때는 DB 세션이 종료되지 않아서 발생 x
- 테스트 코드에서만 발생
지연(Lazy) 방식 | - 데이터를 필요한 시점에 가져오는 방식 |
즉시(Eager) 방식 | - q 객체를 조회할 때 미리 answer 리스트를 모두 가져오는 방식 |
해결
@Transactional
- 메서드가 종료될 때까지 DB 세션이 유지됨
SbbApplicationTests.java
2. 도메인별로 분류하기
- 자바 파일을 도메인에 따라 패키지로 나누어 관리
- 비슷한 기능이나 관련된 개념을 함께 묶어 구조화하여 정리하게 되므로 코드를 쓰거나 읽을 때 유지 보수를 할 때 편리
도메인 이름 | 패키지 이름 | 설명 |
question | com.example.demo.question | 게시판의 질문과 관련된 자바 파일 |
answer | com.example.demo.answer | 게시판의 답변과 관련된 자바 파일 |
user | com.example.demo.user | 사용자의 관련된 자바 파일 |
'SpringBoot' 카테고리의 다른 글
[Do it] 2장 스프링 부트 기본 기능 익히기(5) (0) | 2024.01.21 |
---|---|
[Do it] 2장 스프링 부트 기본 기능 익히기 (4) (0) | 2024.01.21 |
[Do it] 2장 스프링 부트 기본 익히기(3) (0) | 2024.01.19 |
[Do it] 2장 스프링 부트 기본 기능 익히기 (1) (0) | 2024.01.18 |
[Do it] 1장 스프링 부트 개발 준비하기 (1) | 2024.01.18 |