리포지터리
엔티티만으로는 데이터베이스에 데이터를 저장하거나 조회 할 수 없다.
데이터 처리를 위해서는 실제 데이터베이스와 연동하는 JPA 리포지터리가 필요하다.
리포지터리란?
리포지터리는 엔티티에 의해 생성된 데이터베이스 테이블에 접근하는
메소드들(예: findAll, save 등)을 사용하기 위한 인터페이스이다.
데이터 처리를 위해서는 테이블에 어떤 값을 넣거나 값을 조회하는 등의
CRUD(Create, Read, Update, Delete)가 필요하다.
이 때 이러한 CRUD를 어떻게 처리할지 정의하는 계층이 바로 리포지터리이다.
다음과 같이 QuestionRepository 인터페이스를 생성하자.
[파일명:/sbb/src/main/java/com/mysite/sbb/QuestionRepository.java]
package com.mysite.sbb;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Question, Integer> {
}
QuestionRepository는 리포지터리로 만들기 위해 JpaRepository 인터페이스를 상속했다.
JpaRepository를 상속할 때는 제네릭스 타입으로 <Question, Integer> 처럼
리포지터리의 대상이 되는 엔티티의 타입(Question)과 해당 엔티티의 PK의 속성 타입(Integer)을 지정해야 한다.
이것은 JpaRepository를 생성하기 위한 규칙이다.
Question 엔티티의 PK(Primary Key) 속성인 id의 타입은 Integer 이다.
마찬가지로 AnswerRepository도 다음과 같이 생성하자.
[파일명:/sbb/src/main/java/com/mysite/sbb/AnswerRepository.java]
package com.mysite.sbb;
import org.springframework.data.jpa.repository.JpaRepository;
public interface AnswerRepository extends JpaRepository<Answer, Integer> {
}
이제 QuestionRepository, AnswerRepository를 이용하여 question, answer 테이블에 데이터를 저장하거나 조회할 수 있다.
데이터 저장하기
작성한 리포지터리를 테스트하기 위해서 JUnit 기반의 스프링부트의 테스트 프레임워크를 사용해 보자.
위 파일을 열고 다음처럼 수정하자.
[파일명:/sbb/src/test/java/com/mysite/sbb/SbbApplicationTests.java]
package com.mysite.sbb;
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); // 두번째 질문 저장
}
}
@SpringBootTest 어노테이션은 SbbApplicationTests 클래스가 스프링부트 테스트 클래스임을 의미한다.
그리고 @Autowired 애너테이션은 스프링의 DI 기능으로 questionRepository 객체를 스프링이 자동으로 생성해 준다.
DI(Dependency Injection) - 스프링이 객체를 대신 생성하여 주입한다.
@Autowired
객체를 주입하기 위해 사용하는 스프링의 애너테이션이다.
객체를 주입하는 방식에는 @Autowired 외에 Setter 또는 생성자를 사용하는 방식이 있다.
순환참조 문제와 같은 이유로 @Autowired 보다는 생성자를 통한 객체 주입방식이 권장된다.
하지만 테스트 코드의 경우에는 생성자를 통한 객체의 주입이 불가능하므로
테스트 코드 작성시에만 @Autowired를 사용하고 실제 코드 작성시에는 생성자를 통한 객체 주입방식을 사용하겠다.
testJpa 메서드 위의 @Test 어노테이션은 testJpa 메서드가 테스트 메서드임을 나타낸다.
위 클래스를 JUnit으로 실행하면 @Test 어노테이션이 붙은 메서드가 실행된다.
JUnit은 테스트코드를 작성하고 작성한 테스트코드를 실행하기 위해 사용하는 자바의 테스트 프레임워크이다.
testJpa 메서드의 내용을 잠시 살펴보자.
testJpa 메서드는 q1, q2 라는 Question 엔티티 객체를 생성하고
QuestionRepository를 이용하여 그 값을 데이터베이스에 저장하는 코드이다.
이제 작성한 SbbApplicationTests 클래스를 실행해 보자.
다음처럼 [Run -> Run As -> JUnit Test]를 선택하면 SbbApplicationTests 클래스를 실행할수 있다.
하지만 로컬서버가 이미 구동중이라면 The file is locked: nio:/Users/pahkey/local.mv.db 와 비슷한 오류가 발생할 것이다. H2 데이터베이스는 파일 기반의 데이터베이스이기 때문에
이미 로컬서버가 점유하고 있기 때문에 이러한 오류가 발생하는 것이다.
따라서 테스트를 하기 위해서는 로컬 서버를 중지해야 한다.
로컬서버를 중지하고 다시 테스트를 실행해 보자.
그러면 다음과 같은 JUnit 화면이 나타나고 오류없이 잘 실행될 것이다.
(다음 화면처럼 초록색이 표시되면 성공이고 빨간색이 표시되면 실패이다.)
실제 데이터베이스에 값이 잘 들어갔는지 확인해 보기 위해 다시 로컬서버를 시작하고
H2 콘솔에 접속하여 다음 쿼리문을 실행해 보자.
SELECT * FROM QUESTION
그러면 다음처럼 우리가 저장한 Question 객체의 값이 데이터베이스에 저장된 것을 확인한 수 있다.
id는 Question 엔티티의 기본 키(Primary Key)이다.
id는 앞에서 엔티티를 생성할 때 설정했던대로 데이터를 생성할 때 속성값이 자동으로 1씩 증가하는 것을 확인할 수 있다.
Question 엔티티의 id는 @GeneratedValue 설정을 했다.
데이터 조회하기
이번에는 데이터베이스에 저장된 데이터를 조회해 보자.
findAll
작성한 테스트 코드를 다음처럼 수정해 보자.
[파일명:/sbb/src/test/java/com/mysite/sbb/SbbApplicationTests.java]
package com.mysite.sbb;
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());
Question q = all.get(0);
assertEquals("sbb가 무엇인가요?", q.getSubject());
}
}
question 테이블에 저장된 모든 데이터를 조회하기 위해서 리포지터리의 findAll 메서드를 사용했다.
findAll은 데이터를 조회할때 사용하는 메서드이다.
우리는 총 2건의 데이터를 저장했기 때문에 데이터의 사이즈는 2가 되어야 한다.
데이터 사이즈가 2인지 확인하기 위해 JUnit의 assertEquals 메서드를 사용했다.
assertEquals는 assertEquals(기대값, 실제값)와 같이 사용하고 기대값과 실제값이 동일한지를 조사한다.
만약 기대값과 실제값이 동일하지 않다면 테스트는 실패로 처리된다.
그리고 우리가 저장한 첫번째 데이터의 제목이 "sbb가 무엇인가요?"와 일치하는지도 테스트했다.
테스트를 위해서는 로컬 서버를 중지하고 다시한번 [Run -> Run As -> JUnit Test]을 실행하면 된다.
테스트는 잘 통과될 것이다.
우리는 편의상 testJpa 메서드 하나만을 가지고 JPA의 여러기능을 테스트할 것이다.
findById
이번에는 Question 엔티티의 Id값으로 데이터를 조회해 보자. 테스트 코드를 다음과 같이 수정하자.
[파일명:/sbb/src/test/java/com/mysite/sbb/SbbApplicationTests.java]
package com.mysite.sbb;
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() {
Optional<Question> oq = this.questionRepository.findById(1);
if(oq.isPresent()) {
Question q = oq.get();
assertEquals("sbb가 무엇인가요?", q.getSubject());
}
}
}
id 값으로 데이터를 조회하기 위해서는 리포지터리의 findById 메서드를 사용해야 한다.
하지만 findById의 리턴 타입은 Question이 아닌 Optional임에 주의하자.
Optional은 null 처리를 유연하게 처리하기 위해 사용하는 클래스로 위와 같이
isPresent로 null이 아닌지를 확인한 후에 get으로 실제 Question 객체 값을 얻어야 한다.
findBySubject
이번에는 Question 엔티티의 subject 값으로 데이터를 조회해 보자.
하지만 아쉽게도 Question 리포지터리는 findBySubject와 같은 메서드를 기본적으로 제공하지는 않는다.
findBySubject 메서드를 사용하려면 다음처럼 QuestionRepository 인터페이스를 변경해야 한다.
[파일명:/sbb/src/main/java/com/mysite/sbb/QuestionRepository.java]
package com.mysite.sbb;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Question, Integer> {
Question findBySubject(String subject);
}
그러면 다음처럼 제목으로 테이블 데이터를 조회할 수 있다.
[파일명:/sbb/src/test/java/com/mysite/sbb/SbbApplicationTests.java]
package com.mysite.sbb;
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 라는 메서드를 선언만 하고 구현은 하지 않았는데 도대체 어떻게 실행이 되는 거지?"
이러한 마법은 JpaRepository를 상속한 QuestionRepository 객체가 생성될때 벌어진다.
(DI에 의해 스프링이 자동으로 QuestionRepository 객체를 생성한다. 이 때 프록시 패턴이 사용된다고 한다.)
레포지터리 객체의 메서드가 실행될때 JPA가 해당 메서드명을 분석하여 쿼리를 만들고 실행한다.
즉, 여러분은 findBy + 엔티티의 속성명(예:findBySubject)과 같은 리포지터리 메서드를 작성하면
해당 속성의 값으로 데이터를 조회할수 있다.
findBySubject 메서드를 호출할때 실제 어떤 쿼리가 실행되는지 살펴보자.
실행되는 쿼리를 로그에서 보려면 application.properties 파일을 다음과 같이 수정해야 한다.
[파일명:/sbb/src/main/resources/application.properties]
# DATABASE
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.datasource.url=jdbc:h2:~/local
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
# 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
그리고 다시한번 테스트코드를 실행해 보자. 그러면 다음과 같이 콘솔로그에서 실행된 쿼리를 확인할 수 있다.
실행된 쿼리의 where 조건에 subject가 포함된 것을 확인할 수 있다.
findBySubjectAndContent
이번에는 제목과 내용을 함께 조회해 보자.
두 개의 속성을 And 조건으로 조회할때는 레포지터리에 다음과 같은 메서드를 추가해야 한다.
[파일명:/sbb/src/main/java/com/mysite/sbb/QuestionRepository.java]
package com.mysite.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);
}
그리고 테스트코드는 다음과 같이 작성하자.
[파일명:/sbb/src/test/java/com/mysite/sbb/SbbApplicationTests.java]
package com.mysite.sbb;
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.findBySubjectAndContent(
"sbb가 무엇인가요?", "sbb에 대해서 알고 싶습니다.");
assertEquals(1, q.getId());
}
}
테스트는 잘 통과될 것이다.
그리고 실제 쿼리는 다음과 같이 실행되는 것을 콘솔 로그에서 확인할 수 있다.
select
question0_.id as id1_1_,
question0_.content as content2_1_,
question0_.create_date as create_d3_1_,
question0_.subject as subject4_1_
from
question question0_
where
question0_.subject=?
and question0_.content=?
subject, content 컬럼이 and 조건으로 where문에 사용되었다.
이렇듯 레포지터리의 메서드명은 데이터를 조회하는 쿼리문의 where 조건을 결정하는 역할을 한다.
여기서는 findBySubject, findBySubjectAndContent 두 개만 알아봤지만 상당히 많은 조합을 사용할 수 있다.
그것들에 대해서 간단하게 표로 정리해 보았다.
항목예제설명
And | findBySubjectAndContent(String subject, String content) | 여러 컬럼을 and 로 검색 |
Or | findBySubjectOrContent(String subject, String content) | 여러 컬럼을 or 로 검색 |
Between | findByCreateDateBetween(LocalDateTime fromDate, LocalDateTime toDate) | 컬럼을 between으로 검색 |
LessThan | findByIdLessThan(Integer id) | 작은 항목 검색 |
GreaterThanEqual | findByIdGraterThanEqual(Integer id) | 크거나 같은 항목 검색 |
Like | findBySubjectLike(String subject) | like 검색 |
In | findBySubjectIn(String[] subjects) | 여러 값중에 하나인 항목 검색 |
OrderBy | findBySubjectOrderByCreateDateAsc(String subject) | 검색 결과를 정렬하여 전달 |
단, 응답 결과가 여러건인 경우에는 리포지터리 메서드의 리턴 타입을
Question이 아닌 List<Question> 으로 해야 한다.
보다 자세한 내용은 쿼리 생성 규칙에 대한 다음의 공식문서를 참고하자.
- https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation
findBySubjectLike
이번에는 제목에 특정 문자열이 포함되어 있는 데이터를 조회해 보자.
Question 리포지터리를 다음과 같이 수정하자.
[파일명:/sbb/src/main/java/com/mysite/sbb/QuestionRepository.java]
package com.mysite.sbb;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Question, Integer> {
Question findBySubject(String subject);
Question findBySubjectAndContent(String subject, String content);
List<Question> findBySubjectLike(String subject);
}
테스트코드는 다음과 같이 수정하자.
[파일명:/sbb/src/test/java/com/mysite/sbb/SbbApplicationTests.java]
package com.mysite.sbb;
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());
}
}
테스트는 잘 통과될 것이다.
Like 검색을 위해서는 findBySubjectLike 메서드의 입력 문자열로 "sbb%"와 같이 "%"를 적어주어야 한다.
% 표기는 다음과 같은 의미를 갖는다.
- sbb%: "sbb"로 시작하는 문자열
- %sbb: "sbb"로 끝나는 문자열
- %sbb%: "sbb"를 포함하는 문자열
데이터 수정하기
이번에는 질문 데이터를 수정하는 테스트 코드를 작성해 보자.
[파일명:/sbb/src/test/java/com/mysite/sbb/SbbApplicationTests.java]
package com.mysite.sbb;
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);
}
}
assertTrue(값)은 값이 true인지를 테스트한다.
질문 데이터를 조회한 다음 subject를 "수정된 제목" 이라는 값으로 수정했다.
변경된 Question 데이터를 저장하기 위해서는 this.questionRepository.save(q) 처럼
레포지터리의 save 메서드를 사용한다.
테스트를 수행해 보면 콘솔 로그에서 다음과 같은 update 문이 실행되었음을 확인할 수 있을 것이다.
update
question
set
content=?,
create_date=?,
subject=?
where
id=?
데이터 삭제하기
이어서 데이터를 삭제하는 것도 실습해 보자. 여기서는 첫 번째 질문을 삭제해 보자.
[파일명:/sbb/src/test/java/com/mysite/sbb/SbbApplicationTests.java]
package com.mysite.sbb;
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());
}
}
리포지터리의 count() 메서드는 해당 리포지터리의 총 데이터건수를 리턴한다.
Question 레포지터리의 delete 메서드를 사용하여 데이터를 삭제했다.
삭제하기 전에는 데이터 건수가 2, 삭제한 후에는 데이터 건수가 1인지를 테스트했다.
테스트는 잘 통과될 것이다.
답변 데이터 생성 후 저장하기
이번에는 답변(Answer) 데이터를 생성하고 저장해 보자.
[파일명:/sbb/src/test/java/com/mysite/sbb/SbbApplicationTests.java]
package com.mysite.sbb;
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() {
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);
}
}
답변 데이터 처리를 위해서는 답변 레포지터리가 필요하므로 AnswerRepository 객체를 @Autowired로 주입했다.
답변 데이터를 생성하려면 질문 데이터가 필요하므로 우선 질문 데이터를 구해야 한다.
id가 2인 질문 데이터를 가져온 다음 Answer 엔티티의 question 속성에 방금 가져온 질문 데이터를 대입해(a.setQuestion(q)) 답변 데이터를 생성했다.
Answer 엔티티에는 어떤 질문에 해당하는 답변인지 연결할 목적으로 question 속성이 필요하다.
테스트를 수행해 보자.
답변 데이터가 잘 생성될 것이다.
답변 조회하기
Answer도 Question 엔티티와 마찬가지로 id 속성이 기본 키이므로 값이 자동으로 생성된다.
다음처럼 id 값을 이용해 데이터를 조회해 보자.
[파일명:/sbb/src/test/java/com/mysite/sbb/SbbApplicationTests.java]
package com.mysite.sbb;
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());
}
}
id 값이 1인 답변을 조회했다.
그리고 그 답변의 질문 id가 2인지도 테스트해 보았다.
답변에 연결된 질문 찾기 vs 질문에 달린 답변 찾기
앞에서 구성한 Answer 엔티티의 question 속성을 이용하면 "답변에 연결된 질문"을 조회할 수 있다.
a.getQuestion()
답변에 연결된 질문 찾기는 Answer 엔티티에 question 속성이 정의되어 있어서 매우 쉽다.
그런데 반대의 경우도 가능할까?
즉, 질문에서 답변을 찾을수 있을까?
다음과 같이 질문 엔티티에 정의한 answerList를 사용하면 역시 쉽게 구할수 있다.
[파일명:/sbb/src/test/java/com/mysite/sbb/SbbApplicationTests.java]
package com.mysite.sbb;
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;
@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());
}
}
질문 객체로부터 답변 리스트를 구하는 테스트코드이다.
id가 2인 질문에 답변을 한 개 등록했으므로 위와 같이 검증할 수 있다.
하지만 위 코드를 실행하면 다음과 같은 오류가 발생한다.
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.mysite.sbb.Question.answerList, could not initialize proxy - no Session
(... 생략 ...)
왜냐하면 Question 레포지터리가 findById를 호출하여 Question 객체를 조회하고 나면 DB세션이 끊어지기 때문이다.
그 이후에 실행되는 q.getAnswerList() 메서드는 세션이 종료되어 오류가 발생한다.
답변 데이터 리스트는 q 객체를 조회할때 가져오지 않고 q.getAnswerList() 메서드를 호출하는 시점에 가져오기 때문이다.
이렇게 필요한 시점에 데이터를 가져오는 방식을 Lazy 방식이라고 한다.
이와 반대로 q 객체를 조회할때 답변 리스트를 모두 가져오는 방식은 Eager 방식이라고 한다.
@OneToMany, @ManyToOne 어노테이션의 옵션으로
fetch=FetchType.LAZY 또는 fetch=FetchType.EAGER 처럼 가져오는 방식을 설정할 수 있는데
따로 지정하지 않고 항상 디폴트 값을 사용할 것이다.
사실 이 문제는 테스트 코드에서만 발생한다.
실제 서버에서 JPA 프로그램들을 실행할 때는 DB 세션이 종료되지 않기 때문에 위와 같은 오류가 발생하지 않는다.
테스트 코드를 수행할 때 위와 같은 오류를 방지할 수 있는 가장 간단한 방법은
다음처럼 @Transactional 어노테이션을 사용하는 것이다.
@Transactional 어노테이션을 사용하면 메서드가 종료될 때까지 DB 세션이 유지된다.
package com.mysite.sbb;
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;
import org.springframework.transaction.annotation.Transactional;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Transactional
@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());
}
}
위와 같이 testJpa 메서드에 @Transactional 어노테이션을 추가하면 오류없이 잘 수행될 것이다.
'JAVA > SpringBoot CRUD Board' 카테고리의 다른 글
[VSCODE] SpringBoot CRUD게시판 만들기 - 질문 목록과 템플릿 (0) | 2022.07.01 |
---|---|
[VSCODE] SpringBoot CRUD게시판 만들기 - 도메인 별로 분류하기 (0) | 2022.07.01 |
[VSCODE] SpringBoot CRUD게시판 만들기 - 엔티티(Entity) (0) | 2022.07.01 |
[VSCODE] SpringBoot CRUD게시판 만들기 - JPA (0) | 2022.06.30 |
[VSCODE] SpringBoot CRUD게시판 만들기 - 컨트롤러 (0) | 2022.06.30 |