▼ What ?
이전 내용에 이어서 커리큘럼 2주차에 공부한 내용인 리포지터리(Repository), CRUD 연산, 그리고 Fetch type에 대해 추가적으로 공부해보면서 알게 된 'Lazy/Eagar' 방식은 무엇이고 이 둘의 차이점은 무엇인지에 대해서도 정리해보려고 한다.
▼ 리포지터리 (Repository)
리포지터리가 왜 필요할까 ?
- 엔티티만으로는 DB에 데이터를 저장하거나 조회 X
➜ 데이터 처리를 위해서는 실제 DB와 연동하는 JPA 리포지터리가 필요 !
Spring Data JPA
- Spring Data JPA의 핵심 기능이 Repository이며, 후에 설명할 CRUD 연산과 같은 다양한 기능들도 제공해주는 Spring의 라이브러리라는 것을 기억하자.
Repository ?
- 엔티티에 의해 생성된 DB 테이블에 접근하는 메서드들(예: findAll, save 등)을 사용하기 위한 인터페이스,
- 데이터 처리를 위해서는 테이블에 어떤 값을 넣거나 값을 조회하는 등의 CRUD(Create, Read, Update, Delete)가 필요하다.
➜ 이 때 이러한 CRUD를 어떻게 처리할지를 정의하는 계층이 바로 리포지토리(Repository)이다 !
Repository 인터페이스 생성
- 해당 인터페이스를 리포지터리로 만들려면 'JpaRepository' 인터페이스를 상속하도록 한다.
➜ 'JpaRepository'를 상속할 때엔 지네릭스 타입으로 리포지터리의 대상이 되는 엔티티의 타입과 해당 엔티티의 PK(Primary Key)의 속성 타입을 지정해줘야 한다 !
package com.gdsc.webboard;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Answer, Integer> {
}
🔻 Answer 리포지터리도 이와 같이 생성해주면, 'QuestionRepository', 'AnswerRepository'를 이용하여 question, answer 테이블에 데이터를 저장하거나 조회하는 것이 가능 !
▼ CRUD
데이터 저장
- JUnit 기반의 스프링부트의 테스트 프레임워크를 사용한 리포지터리 테스트.
( JUnit은 테스트코드를 작성하고 작성한 테스트코드를 실행하기 위해 사용하는 자바의 테스트 프레임워크이다. )
➜ '@Test' 애너테이션이 붙은 메서드가 실행된다.
package com.gdsc.webboard;
import ...
@SpringBootTest // 스프링부트의 테스트 클래스임을 의미
class WebBoardApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test // testJpa 메서드가 테스트 메서드임을 나타낸다
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); // 두번째 질문 저장
}
}
🔻 @Autowired : 객체를 주입하기 위해 사용하는 Spring의 애너테이션 (Spring의 DI 기능)
- 객체를 주입하는 방식에는 '@Autowired' 외에 Setter 또는 생성자를 사용하는 방식이 있다.
➜ 순환참조 문제와 같은 이유로 '@Autowired' 보다는 생성자를 통한 객체 주입방식이 권장되지만, 테스트 코드의 경우에는 생성자를 통한 객체의 주입이 불가능하므로 테스트 코드 작성시에만 @Autowired를 사용
DI & @Autowired (참고 자료) - [Spring] DI & @Autowired — Uykm_Note (tistory.com)
[Spring] DI & @Autowired
▼ Why ? What ? ▼ DI (Dependency Injection) 의존성 종속; class 간의 의존관계를 Spring Container가 자동으로 연결해주는 것 ➜ class A가 class B, C와 상호작용한다면, 객체 A는 객체 B, C와 의존관계이다 의존관
ukym-tistory.tistory.com
- 테스트 클래스를 실행할 땐, 로컬서버를 중지하고 다시 테스트를 실행시키자.
( testJpa 메서드는 q1, q2 라는 Question 엔티티 객체를 생성하고 QuestionRepository를 이용하여 그 값을 데이터베이스에 저장하는 코드이다. )
➜ 다시 로컬서버를 시작하고 다음과 같은 쿼리문(SELECT * FROM QUESTION)을 실행시켜 실제 DB에 값이 잘 들어갔는지 확인 !

🔻 id(Question 엔티티의 PK)는 앞에서 엔티티를 생성할 때 설정(@GeneratedValue)했던대로 데이터를 생성할 때 속성값이 자동으로 1씩 증가하는 것을 확인할 수 있다.
데이터 조회
- findAll : 테이블에 저장된 모든 데이터를 조회할 때 사용하는 메서드.
package com.gdsc.webboard;
import ...
@SpringBootTest
class WebBoardApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
List<Question> all = this.questionRepository.findAll();
assertEquals(2, all.size()); // 데이터 사이즈가 2인지
Question q = all.get(0);
assertEquals("sbb가 무엇인가요?", q.getSubject()); // 우리가 저장한 첫 번째 데이터의 제목이 "sbb가 무엇인가요?"인지
}
}
🔻 assertEquals(기대값, 실제값) : 기대값과 실제값이 동일한지 조사하는 메서드, 동일하지 않으면 해당 테스트는 실패로 처리한다.
- findById : id(Question 엔티티의 PK) 값으로 데이터를 조회
package com.gdsc.webboard;
import java.util.Optional;
import ...
@SpringBootTest
class WebBoardApplicationTests {
@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());
}
}
}
🔻 리턴 타입이 Optional ?
Optional은 null 처리를 유연하게 처리하기 위해 사용하는 클래스로, 위와 같이 isPresent로 null이 아닌지를 확인한 후에 get으로 실제 Question 객체 값을 얻어야 한다.
- findBySubject : Question 엔티티의 subject 속성의 값으로 데이터를 조회.
➜ 이 메서드는 리포지터리(QuestionRepository)에서 기본적으로 제공하지 X
➜ findBySubject 메서드를 사용하려면 해당 리포지터리 인터페이스(QuestionRepository)를 변경해야 한다.
package com.gdsc.webboard;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Question, Integer> {
Question findBySubject(String subject);
}
🔻 이렇게 리포지터리 인터페이스를 변경해주면, 아래처럼 제목(subject)으로 테이블의 데이터를 조회할 수 있게 된다.
package com.gdsc.webboard;
import ...
@SpringBootTest
class WebBoardApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
Question q = this.questionRepository.findBySubject("sbb가 무엇인가요?");
assertEquals(1, q.getId());
}
}
🔻 근데 findBySubject를 구현한 것이 아닌 선언만 했을 뿐인데 어떻게 사용할 수 있는거지 ?
JpaRepository를 상속한 QuestionRepository 객체가 생성될 때, DI에 의해 스프링이 자동으로 QuestionRepository 객체를 생성
➜ 해당 리포지터리 객체를 통해 메서드가 호출될 때 JPA가 해당 메서드명을 분석하여 쿼리를 만들고 실행한다. (프록시 패턴*이 사용)
➜ 즉, 'findBy + 엔터티의 속성명' (ex. findBySubject)과 같은 리포지터리의 메서드를 선언해두면 해당 속성의 값으로 데이터 조회 가능 !
* 프록시 패턴 (Proxy Pattern)
: 꼭 필요로 하는 시점까지 객체의 생성을 연기하고, 해당 객체가 생성된 것 처럼 동작하도록 만들고 싶을 때 사용하는 패턴
➜ 실행되는 쿼리를 로그에서 보려면 '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 // 추가

🔻 쿼리문에서 where문에 subject 칼럼이 조건으로 사용된 것을 확인해볼 수 있다.
- findBySubjectAndContent : 제목과 내용으로 데이터를 조회.
➜ 리포지터리(QuestionRepository) 수정.
package com.gdsc.webboard;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Question, Integer> {
Question findBySubject(String subject);
Question findBySubjectAndContent(String subject, String content);
}
➜ 테스트코드 수정.
package com.gdsc.webboard;
import ...
@SpringBootTest
class WebBoardApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
Question q = this.questionRepository.findBySubjectAndContent(
"sbb가 무엇인가요?", "sbb에 대해서 알고 싶습니다.");
assertEquals(1, q.getId());
}
}

🔻 쿼리문에서 where문에 subject, content 컬럼이 and 조건으로 사용된 것을 확인할 수 있다.
➜ 이처럼 리포지터리의 메서드명은 데이터를 조회하는 쿼리문의 where 조건을 결정하는 역할을 한다.
- 'And' 이외에도 아래처럼 다양한 조합을 사용 가능하다 !
항목 | 예제 | 설명 |
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) | 검색 결과를 정렬하여 전달 |
- findBySubjectLike
: 제목에 특정 문자열이 포함되어 있는 데이터를 조회.
➜ 리포지터리(QuestionRepository) 수정.
package com.gdsc.webboard;
import java.util.List;
import ...
public interface QuestionRepository extends JpaRepository<Question, Integer> {
Question findBySubject(String subject);
Question findBySubjectAndContent(String subject, String content);
List<Question> findBySubjectLike(String subject);
}
➜ 테스트코드 수정.
- sbb%: "sbb"로 시작하는 문자열.
- %sbb: "sbb"로 끝나는 문자열.
- %sbb%: "sbb"를 포함하는 문자열.
package com.gdsc.webboard;
import java.util.List;
import ...
@SpringBootTest
class WebBoardApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
List<Question> qList = this.questionRepository.findBySubjectLike("sbb%");
Question q = qList.get(0);
assertEquals("sbb가 무엇인가요?", q.getSubject());
}
}
🔻 위처럼 응답 결과가 여러 개일 수 있는 경우엔 리포지터리 메서드의 리턴 타입을 'List<Question>'으로 해야 한다.
데이터 수정
- 질문(Question) 데이터를 수정하는 테스트 코드.
➜ save(데이터) : 리포지터리의 save 메서드를 이용하여 변경된 데이터 저장.
package com.gdsc.webboard;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Optional;
import ...
@SpringBootTest
class WebBoardApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
Optional<Question> oq = this.questionRepository.findById(1); // Question 데이터 조회
assertTrue(oq.isPresent());
Question q = oq.get();
q.setSubject("수정된 제목"); // 조회한 데이터를 "수정된 제목"으로 제목 수정
this.questionRepository.save(q);
}
}

🔻 쿼리문을 보면 update문이 실행됐음을 알 수 있다.
🔻 assertTrue(값) : 값이 true인지 테스트.
데이터 삭제
- 삭제하기 전에는 데이터 건수가 2, 삭제한 후에는 데이터 건수가 1인지 테스트.
➜ delete(데이터) : 리포지터리의 delete 메서드를 이용하여 데이터를 삭제하는 메서드.
package com.gdsc.webboard;
import static org.junit.jupiter.api.Assertions.assertEquals;
import ...
@SpringBootTest
class WebBoardApplicationTests {
@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() : 리포지터리의 count() 메서드는 해당 리포지터리의 총 데이터건수를 리턴하는 메서드.
답변 데이터 생성 후 저장
- 답변(Answer) 데이터를 생성하고 저장해보자.
➜ 답변 데이터 처리를 위해선 Answer 리포지터리가 필요하므로, '@Autowired' 애너테이션으로 AnswerRepository 객체를 주입 !
➜ 답변 데이터를 생성하기 위해선 먼저 질문(Question) 데이터가 필요하다.
package com.gdsc.webboard;
import java.time.LocalDateTime;
import ...
@SpringBootTest
class WebBoardApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Autowired
private AnswerRepository answerRepository;
@Test
void testJpa() {
Optional<Question> oq = this.questionRepository.findById(2); // id가 2인 Question 데이터 조회
assertTrue(oq.isPresent());
Question q = oq.get();
Answer a = new Answer();
a.setContent("네 자동으로 생성됩니다.");
// Answer 엔티티의 question 속성에 Question 객체(id가 2인 데이터) 대입
// @ManyToOne
// private Question question;
a.setQuestion(q); // 어떤 질문의 답변인지 알기 위해서 Question 객체가 필요
a.setCreateDate(LocalDateTime.now());
this.answerRepository.save(a);
}
}
답변 조회
- Answer도 Question 엔티티와 마찬가지로 id 속성이 기본 키(@Id)이므로 값이 자동으로 생성된다.
➜ id 값을 이용해 데이터를 조회하면 된다.
package com.gdsc.webboard;
import ...
@SpringBootTest
class WebBoardApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Autowired
private AnswerRepository answerRepository;
@Test
void testJpa() {
Optional<Answer> oa = this.answerRepository.findById(1); // id가 1인 답변(Answer) 조회
assertTrue(oa.isPresent());
Answer a = oa.get();
assertEquals(2, a.getQuestion().getId()); // 그 답변의 질문 id가 2인지 테스트
}
}
답변에 연결된 질문 찾기 vs 질문에 달린 답변 찾기 (+ Fetch type)
- Answer 엔티티에 정의되어 있는 question 속성을 통해 아래 메서드를 이용하면 "답변에 연결된 질문"을 쉽게 조회할 수 있다.
a.getQuestion()
- 반대로, Question 엔티티에 정의한 'answerList'를 사용하면 "질문에 달린 답변"도 쉽게 조회할 수 있다.
➜ id가 2인 질문에 답변을 1개 등록했으므로 아래와 같이 테스트 가능.
package com.gdsc.webboard;
import java.util.List;
import ...
@SpringBootTest
class WebBoardApplicationTests {
@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()); // id가 2인 질문에 답변이 1개 이기 때문에 체크
assertEquals("네 자동으로 생성됩니다.", answerList.get(0).getContent());
}
}
🔻 하지만, 아래와 같은 오류 발생 !
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.gdsc.webboard.Question.answerList: could not initialize proxy - no Session
➜ Question 리포지터리가 findById 메서드를 호출하여 Question 객체를 조회하고 나면 DB세션이 끊어지는데, 답변 데이터 리스트는 q 객체를 조회(questionRepository.findById(2))할 때 가져오지 않고 q.getAnswerList()메서드를 호출하는 시점, 즉 이미 세션이 종료된 시점에 가져오려 하기 때문에 오류가 발생한다 !
➜ 이처럼 필요한 시점에 데이터를 가져오는 방식(게으른)을 Lazy 로딩 방식이라 한다
( 반대로, q 객체를 조회할 때 답변 리스트를 모두 가져오게 하는 방식(미리)이 Eager 로딩 방식이라 한다 )
Default fetch type in JPA (And in Hibernate)
- OneToMany: LAZY
- ManyToOne: EAGER
- ManyToMany: LAZY
- OneToOne: EAGER
Eager(즉시) 로딩 방식은 불필요한 객체들까지 모두 가져와 성능 저하가 발생하기 때문에, 보편적으로는 Lazy(지연) 로딩 방식을 사용한다 !
➜ 이에 대한 해결 방법은 ?
'@OneToMany', '@ManyToOne'애너테이션의 옵션으로 'fetch=FetchType.LAZY' 또는 'fetch=FetchType.EAGER' 처럼 가져오는 방식을 설정하면 해결되지만, 이번 커리큘럼에선 항상 디폴트(Default) 값을 사용할 것이다.
- 사실 이 문제는 테스트 코드에서만 발생한다.
➜ 실제 서버에서 JPA 프로그램들을 실행할 때는 DB 세션이 종료되지 않기 때문에 위와 같은 오류가 발생 X
" 테스트 코드를 수행할 때 위와 같은 오류를 방지할 수 있는 간단한 방법 "
➜ '@Transactional' 사용 ~ 메서드가 종료될 때까지 DB 세션이 유지된다.
package com.gdsc.webboard;
import org.springframework.transaction.annotation.Transactional;
import ...
@SpringBootTest
class WebBoardApplicationTests {
@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()); // id가 2인 질문에 답변이 1개 이기 때문에 체크
assertEquals("네 자동으로 생성됩니다.", answerList.get(0).getContent());
}
}
▼ What ?
이전 내용에 이어서 커리큘럼 2주차에 공부한 내용인 리포지터리(Repository), CRUD 연산, 그리고 Fetch type에 대해 추가적으로 공부해보면서 알게 된 'Lazy/Eagar' 방식은 무엇이고 이 둘의 차이점은 무엇인지에 대해서도 정리해보려고 한다.
▼ 리포지터리 (Repository)
리포지터리가 왜 필요할까 ?
- 엔티티만으로는 DB에 데이터를 저장하거나 조회 X
➜ 데이터 처리를 위해서는 실제 DB와 연동하는 JPA 리포지터리가 필요 !
Spring Data JPA
- Spring Data JPA의 핵심 기능이 Repository이며, 후에 설명할 CRUD 연산과 같은 다양한 기능들도 제공해주는 Spring의 라이브러리라는 것을 기억하자.
Repository ?
- 엔티티에 의해 생성된 DB 테이블에 접근하는 메서드들(예: findAll, save 등)을 사용하기 위한 인터페이스,
- 데이터 처리를 위해서는 테이블에 어떤 값을 넣거나 값을 조회하는 등의 CRUD(Create, Read, Update, Delete)가 필요하다.
➜ 이 때 이러한 CRUD를 어떻게 처리할지를 정의하는 계층이 바로 리포지토리(Repository)이다 !
Repository 인터페이스 생성
- 해당 인터페이스를 리포지터리로 만들려면 'JpaRepository' 인터페이스를 상속하도록 한다.
➜ 'JpaRepository'를 상속할 때엔 지네릭스 타입으로 리포지터리의 대상이 되는 엔티티의 타입과 해당 엔티티의 PK(Primary Key)의 속성 타입을 지정해줘야 한다 !
package com.gdsc.webboard;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Answer, Integer> {
}
🔻 Answer 리포지터리도 이와 같이 생성해주면, 'QuestionRepository', 'AnswerRepository'를 이용하여 question, answer 테이블에 데이터를 저장하거나 조회하는 것이 가능 !
▼ CRUD
데이터 저장
- JUnit 기반의 스프링부트의 테스트 프레임워크를 사용한 리포지터리 테스트.
( JUnit은 테스트코드를 작성하고 작성한 테스트코드를 실행하기 위해 사용하는 자바의 테스트 프레임워크이다. )
➜ '@Test' 애너테이션이 붙은 메서드가 실행된다.
package com.gdsc.webboard;
import ...
@SpringBootTest // 스프링부트의 테스트 클래스임을 의미
class WebBoardApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test // testJpa 메서드가 테스트 메서드임을 나타낸다
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); // 두번째 질문 저장
}
}
🔻 @Autowired : 객체를 주입하기 위해 사용하는 Spring의 애너테이션 (Spring의 DI 기능)
- 객체를 주입하는 방식에는 '@Autowired' 외에 Setter 또는 생성자를 사용하는 방식이 있다.
➜ 순환참조 문제와 같은 이유로 '@Autowired' 보다는 생성자를 통한 객체 주입방식이 권장되지만, 테스트 코드의 경우에는 생성자를 통한 객체의 주입이 불가능하므로 테스트 코드 작성시에만 @Autowired를 사용
DI & @Autowired (참고 자료) - [Spring] DI & @Autowired — Uykm_Note (tistory.com)
[Spring] DI & @Autowired
▼ Why ? What ? ▼ DI (Dependency Injection) 의존성 종속; class 간의 의존관계를 Spring Container가 자동으로 연결해주는 것 ➜ class A가 class B, C와 상호작용한다면, 객체 A는 객체 B, C와 의존관계이다 의존관
ukym-tistory.tistory.com
- 테스트 클래스를 실행할 땐, 로컬서버를 중지하고 다시 테스트를 실행시키자.
( testJpa 메서드는 q1, q2 라는 Question 엔티티 객체를 생성하고 QuestionRepository를 이용하여 그 값을 데이터베이스에 저장하는 코드이다. )
➜ 다시 로컬서버를 시작하고 다음과 같은 쿼리문(SELECT * FROM QUESTION)을 실행시켜 실제 DB에 값이 잘 들어갔는지 확인 !

🔻 id(Question 엔티티의 PK)는 앞에서 엔티티를 생성할 때 설정(@GeneratedValue)했던대로 데이터를 생성할 때 속성값이 자동으로 1씩 증가하는 것을 확인할 수 있다.
데이터 조회
- findAll : 테이블에 저장된 모든 데이터를 조회할 때 사용하는 메서드.
package com.gdsc.webboard;
import ...
@SpringBootTest
class WebBoardApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
List<Question> all = this.questionRepository.findAll();
assertEquals(2, all.size()); // 데이터 사이즈가 2인지
Question q = all.get(0);
assertEquals("sbb가 무엇인가요?", q.getSubject()); // 우리가 저장한 첫 번째 데이터의 제목이 "sbb가 무엇인가요?"인지
}
}
🔻 assertEquals(기대값, 실제값) : 기대값과 실제값이 동일한지 조사하는 메서드, 동일하지 않으면 해당 테스트는 실패로 처리한다.
- findById : id(Question 엔티티의 PK) 값으로 데이터를 조회
package com.gdsc.webboard;
import java.util.Optional;
import ...
@SpringBootTest
class WebBoardApplicationTests {
@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());
}
}
}
🔻 리턴 타입이 Optional ?
Optional은 null 처리를 유연하게 처리하기 위해 사용하는 클래스로, 위와 같이 isPresent로 null이 아닌지를 확인한 후에 get으로 실제 Question 객체 값을 얻어야 한다.
- findBySubject : Question 엔티티의 subject 속성의 값으로 데이터를 조회.
➜ 이 메서드는 리포지터리(QuestionRepository)에서 기본적으로 제공하지 X
➜ findBySubject 메서드를 사용하려면 해당 리포지터리 인터페이스(QuestionRepository)를 변경해야 한다.
package com.gdsc.webboard;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Question, Integer> {
Question findBySubject(String subject);
}
🔻 이렇게 리포지터리 인터페이스를 변경해주면, 아래처럼 제목(subject)으로 테이블의 데이터를 조회할 수 있게 된다.
package com.gdsc.webboard;
import ...
@SpringBootTest
class WebBoardApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
Question q = this.questionRepository.findBySubject("sbb가 무엇인가요?");
assertEquals(1, q.getId());
}
}
🔻 근데 findBySubject를 구현한 것이 아닌 선언만 했을 뿐인데 어떻게 사용할 수 있는거지 ?
JpaRepository를 상속한 QuestionRepository 객체가 생성될 때, DI에 의해 스프링이 자동으로 QuestionRepository 객체를 생성
➜ 해당 리포지터리 객체를 통해 메서드가 호출될 때 JPA가 해당 메서드명을 분석하여 쿼리를 만들고 실행한다. (프록시 패턴*이 사용)
➜ 즉, 'findBy + 엔터티의 속성명' (ex. findBySubject)과 같은 리포지터리의 메서드를 선언해두면 해당 속성의 값으로 데이터 조회 가능 !
* 프록시 패턴 (Proxy Pattern)
: 꼭 필요로 하는 시점까지 객체의 생성을 연기하고, 해당 객체가 생성된 것 처럼 동작하도록 만들고 싶을 때 사용하는 패턴
➜ 실행되는 쿼리를 로그에서 보려면 '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 // 추가

🔻 쿼리문에서 where문에 subject 칼럼이 조건으로 사용된 것을 확인해볼 수 있다.
- findBySubjectAndContent : 제목과 내용으로 데이터를 조회.
➜ 리포지터리(QuestionRepository) 수정.
package com.gdsc.webboard;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Question, Integer> {
Question findBySubject(String subject);
Question findBySubjectAndContent(String subject, String content);
}
➜ 테스트코드 수정.
package com.gdsc.webboard;
import ...
@SpringBootTest
class WebBoardApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
Question q = this.questionRepository.findBySubjectAndContent(
"sbb가 무엇인가요?", "sbb에 대해서 알고 싶습니다.");
assertEquals(1, q.getId());
}
}

🔻 쿼리문에서 where문에 subject, content 컬럼이 and 조건으로 사용된 것을 확인할 수 있다.
➜ 이처럼 리포지터리의 메서드명은 데이터를 조회하는 쿼리문의 where 조건을 결정하는 역할을 한다.
- 'And' 이외에도 아래처럼 다양한 조합을 사용 가능하다 !
항목 | 예제 | 설명 |
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) | 검색 결과를 정렬하여 전달 |
- findBySubjectLike
: 제목에 특정 문자열이 포함되어 있는 데이터를 조회.
➜ 리포지터리(QuestionRepository) 수정.
package com.gdsc.webboard;
import java.util.List;
import ...
public interface QuestionRepository extends JpaRepository<Question, Integer> {
Question findBySubject(String subject);
Question findBySubjectAndContent(String subject, String content);
List<Question> findBySubjectLike(String subject);
}
➜ 테스트코드 수정.
- sbb%: "sbb"로 시작하는 문자열.
- %sbb: "sbb"로 끝나는 문자열.
- %sbb%: "sbb"를 포함하는 문자열.
package com.gdsc.webboard;
import java.util.List;
import ...
@SpringBootTest
class WebBoardApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
List<Question> qList = this.questionRepository.findBySubjectLike("sbb%");
Question q = qList.get(0);
assertEquals("sbb가 무엇인가요?", q.getSubject());
}
}
🔻 위처럼 응답 결과가 여러 개일 수 있는 경우엔 리포지터리 메서드의 리턴 타입을 'List<Question>'으로 해야 한다.
데이터 수정
- 질문(Question) 데이터를 수정하는 테스트 코드.
➜ save(데이터) : 리포지터리의 save 메서드를 이용하여 변경된 데이터 저장.
package com.gdsc.webboard;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Optional;
import ...
@SpringBootTest
class WebBoardApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
Optional<Question> oq = this.questionRepository.findById(1); // Question 데이터 조회
assertTrue(oq.isPresent());
Question q = oq.get();
q.setSubject("수정된 제목"); // 조회한 데이터를 "수정된 제목"으로 제목 수정
this.questionRepository.save(q);
}
}

🔻 쿼리문을 보면 update문이 실행됐음을 알 수 있다.
🔻 assertTrue(값) : 값이 true인지 테스트.
데이터 삭제
- 삭제하기 전에는 데이터 건수가 2, 삭제한 후에는 데이터 건수가 1인지 테스트.
➜ delete(데이터) : 리포지터리의 delete 메서드를 이용하여 데이터를 삭제하는 메서드.
package com.gdsc.webboard;
import static org.junit.jupiter.api.Assertions.assertEquals;
import ...
@SpringBootTest
class WebBoardApplicationTests {
@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() : 리포지터리의 count() 메서드는 해당 리포지터리의 총 데이터건수를 리턴하는 메서드.
답변 데이터 생성 후 저장
- 답변(Answer) 데이터를 생성하고 저장해보자.
➜ 답변 데이터 처리를 위해선 Answer 리포지터리가 필요하므로, '@Autowired' 애너테이션으로 AnswerRepository 객체를 주입 !
➜ 답변 데이터를 생성하기 위해선 먼저 질문(Question) 데이터가 필요하다.
package com.gdsc.webboard;
import java.time.LocalDateTime;
import ...
@SpringBootTest
class WebBoardApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Autowired
private AnswerRepository answerRepository;
@Test
void testJpa() {
Optional<Question> oq = this.questionRepository.findById(2); // id가 2인 Question 데이터 조회
assertTrue(oq.isPresent());
Question q = oq.get();
Answer a = new Answer();
a.setContent("네 자동으로 생성됩니다.");
// Answer 엔티티의 question 속성에 Question 객체(id가 2인 데이터) 대입
// @ManyToOne
// private Question question;
a.setQuestion(q); // 어떤 질문의 답변인지 알기 위해서 Question 객체가 필요
a.setCreateDate(LocalDateTime.now());
this.answerRepository.save(a);
}
}
답변 조회
- Answer도 Question 엔티티와 마찬가지로 id 속성이 기본 키(@Id)이므로 값이 자동으로 생성된다.
➜ id 값을 이용해 데이터를 조회하면 된다.
package com.gdsc.webboard;
import ...
@SpringBootTest
class WebBoardApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Autowired
private AnswerRepository answerRepository;
@Test
void testJpa() {
Optional<Answer> oa = this.answerRepository.findById(1); // id가 1인 답변(Answer) 조회
assertTrue(oa.isPresent());
Answer a = oa.get();
assertEquals(2, a.getQuestion().getId()); // 그 답변의 질문 id가 2인지 테스트
}
}
답변에 연결된 질문 찾기 vs 질문에 달린 답변 찾기 (+ Fetch type)
- Answer 엔티티에 정의되어 있는 question 속성을 통해 아래 메서드를 이용하면 "답변에 연결된 질문"을 쉽게 조회할 수 있다.
a.getQuestion()
- 반대로, Question 엔티티에 정의한 'answerList'를 사용하면 "질문에 달린 답변"도 쉽게 조회할 수 있다.
➜ id가 2인 질문에 답변을 1개 등록했으므로 아래와 같이 테스트 가능.
package com.gdsc.webboard;
import java.util.List;
import ...
@SpringBootTest
class WebBoardApplicationTests {
@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()); // id가 2인 질문에 답변이 1개 이기 때문에 체크
assertEquals("네 자동으로 생성됩니다.", answerList.get(0).getContent());
}
}
🔻 하지만, 아래와 같은 오류 발생 !
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.gdsc.webboard.Question.answerList: could not initialize proxy - no Session
➜ Question 리포지터리가 findById 메서드를 호출하여 Question 객체를 조회하고 나면 DB세션이 끊어지는데, 답변 데이터 리스트는 q 객체를 조회(questionRepository.findById(2))할 때 가져오지 않고 q.getAnswerList()메서드를 호출하는 시점, 즉 이미 세션이 종료된 시점에 가져오려 하기 때문에 오류가 발생한다 !
➜ 이처럼 필요한 시점에 데이터를 가져오는 방식(게으른)을 Lazy 로딩 방식이라 한다
( 반대로, q 객체를 조회할 때 답변 리스트를 모두 가져오게 하는 방식(미리)이 Eager 로딩 방식이라 한다 )
Default fetch type in JPA (And in Hibernate)
- OneToMany: LAZY
- ManyToOne: EAGER
- ManyToMany: LAZY
- OneToOne: EAGER
Eager(즉시) 로딩 방식은 불필요한 객체들까지 모두 가져와 성능 저하가 발생하기 때문에, 보편적으로는 Lazy(지연) 로딩 방식을 사용한다 !
➜ 이에 대한 해결 방법은 ?
'@OneToMany', '@ManyToOne'애너테이션의 옵션으로 'fetch=FetchType.LAZY' 또는 'fetch=FetchType.EAGER' 처럼 가져오는 방식을 설정하면 해결되지만, 이번 커리큘럼에선 항상 디폴트(Default) 값을 사용할 것이다.
- 사실 이 문제는 테스트 코드에서만 발생한다.
➜ 실제 서버에서 JPA 프로그램들을 실행할 때는 DB 세션이 종료되지 않기 때문에 위와 같은 오류가 발생 X
" 테스트 코드를 수행할 때 위와 같은 오류를 방지할 수 있는 간단한 방법 "
➜ '@Transactional' 사용 ~ 메서드가 종료될 때까지 DB 세션이 유지된다.
package com.gdsc.webboard;
import org.springframework.transaction.annotation.Transactional;
import ...
@SpringBootTest
class WebBoardApplicationTests {
@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()); // id가 2인 질문에 답변이 1개 이기 때문에 체크
assertEquals("네 자동으로 생성됩니다.", answerList.get(0).getContent());
}
}