본문 바로가기
JPA

[JPA] jpa 메서드

by 개발LOG 2024. 2. 12.

 

질문 데이터 저장하기

1) 질문 엔티티로 테이블을 만들었으니 이제 만들어진 테이블에 데이터를 생성하고 저장해 보자. 먼저, src/test/java 디렉터리의 com.mysite.sbb 패키지에 SbbApplicationTests.java 파일을 열어 보자.

2) 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 클래스가 스프링 부트의 테스트 클래스임을 의미한다. 그리고 질문 엔티티의 데이터를 생성할 때 리포지터리(여기서는 QuestionRepository)가 필요하므로 @Autowired 애너테이션을 통해 스프링의 ‘의존성 주입(DI)’이라는 기능을 사용하여 QuestionRepository의 객체를 주입했다.

스프링의 의존성 주입(DI, Dependency Injection)이란 스프링이 객체를 대신 생성하여 주입하는 기법을 말한다.

점프 투 스프링부트@Autowired 애너테이션을 더 알아보자.

앞서 작성한 테스트 코드를 보면 questionRepository 변수는 선언만 되어 있고 그 값이 비어 있다. 하지만 @Autowired 애너테이션을 해당 변수에 적용하면 스프링 부트가 questionRepository 객체를 자동으로 만들어 주입한다. 객체를 주입하는 방식에는 @Autowired 애너테이션을 사용하는 것 외에 Setter 메서드 또는 생성자를 사용하는 방식이 있다. 순환 참조 문제와 같은 이유로 개발 시 @Autowired보다는 생성자를 통한 객체 주입 방식을 권장한다. 하지만 테스트 코드의 경우 JUnit이 생성자를 통한 객체 주입을 지원하지 않으므로 테스트 코드 작성 시에만 @Autowired를 사용하고 실제 코드 작성 시에는 생성자를 통한 객체 주입 방식을 사용해 보자.

@Test 애너테이션은 testJpa 메서드가 테스트 메서드임을 나타낸다. SbbApplicationTests 클래스를 JUnit으로 실행하면 @Test 애너테이션이 붙은 testJpa 메서드가 실행된다.

testJpa 메서드의 내용을 자세히 살펴보자. testJpa 메서드는 q1, q2라는 질문 엔티티의 객체를 생성하고 QuestionRepository를 이용하여 그 값을 데이터베이스에 저장한다. 이와 같이 데이터를 저장하면 H2 데이터베이스의 question 테이블은 다음과 같은 형태로 저장될 것이다.

IDContentCreateDateSubject

1 sbb에 대해서 알고 싶습니다. 2023-09-01-15:30:30 sbb가 무엇인가요?
2 id는 자동으로 생성되나요? 2023-09-01-15:30:30 스프링 부트 모델 질문입니다.

3) 이제 작성한 SbbApplicationTests 클래스를 실행해 보자. 다음과 같이 [Run → Run As → JUnit Test] 순서대로 선택하면 SbbApplicationTests 클래스를 실행할 수 있다.

4) 하지만 로컬 서버가 이미 구동 중이라면 The file is locked: nio:/Users/pahkey/local.mv.db와 비슷한 오류가 발생할 것이다. H2 데이터베이스는 파일 기반의 데이터베이스인데, 이미 로컬 서버가 동일한 데이터베이스 파일(local.mv.db)을 점유하고 있어 이러한 오류가 발생하는 것이다. 따라서 테스트할 때는 먼저 로컬 서버를 중지해야 한다. 로컬 서버는 다음과 같이 Boot Dashboard에서 중지 버튼을 클릭하여 중지할 수 있다.

5) 만약 오류가 발생했다면 로컬 서버를 중지하고 [Run → Run]을 클릭한 뒤, 다시 테스트를 실행해 보자. 그러면 다음과 같은 JUnit 화면이 나타나고 오류 없이 잘 실행된다.

여기에 초록색 바가 표시되면 성공이고 빨간색 바가 표시되면 실패를 의미한다.

6) 실제 데이터베이스에 값이 잘 들어갔는지 확인해 보기 위해 다시 로컬 서버를 시작하고 H2 콘솔에 접속하여 다음 쿼리문을 실행해 보자.

SELECT * FROM QUESTION

그러면 다음과 같이 우리가 저장한 Question 객체의 값이 데이터베이스의 데이터로 저장된 것을 확인할 수 있다.

이 책은 SQL에 관한 책이 아니므로 이 쿼리문을 간단하게 설명하면 question 테이블의 모든 행을 조회한다는 의미로 다음과 같이 작성되었다. 참고로 *은 모든 열을 의미한다.

id는 질문 엔티티의 기본키로, 2-04절에서 질문 엔티티를 생성할 때 @GeneratedValue를 활용해 설정했던 대로 속성값이 자동으로 1씩 증가하는 것을 확인할 수 있다.

질문 데이터 조회하기

리포지터리가 제공하는 메서드들을 하나씩 살펴보고 이를 활용해 데이터를 조회해 보자.

findAll 메서드

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 테이블에 저장된 모든 데이터를 조회하기 위해서 리포지터리(questionRepository)의 findAll 메서드를 사용했다.

findAll 메서드는 H2 콘솔에서 입력해 본 SELECT * FROM QUESTION과 같은 결과를 얻을 수 있다.

우리는 앞서 2개의 질문 데이터를 저장했기 때문에 데이터 사이즈는 2가 되어야 한다. 데이터 사이즈가 2인지 확인하기 위해 JUnit의 assertEquals 메서드를 사용하는데, 이 메서드는 테스트에서 예상한 결과와 실제 결과가 동일한지를 확인하는 목적으로 사용한다. 즉, JPA 또는 데이터베이스에서 데이터를 올바르게 가져오는지를 확인하려는 것이다. assertEquals(기댓값, 실젯값)와 같이 작성하고 기댓값과 실젯값이 동일한지를 조사한다. 만약 기댓값과 실젯값이 동일하지 않다면 테스트는 실패로 처리된다. 여기서는 우리가 저장한 첫 번째 데이터의 제목이 'sbb가 무엇인가요?' 데이터와 일치하는지도 테스트했다. 테스트할 때 로컬 서버를 중지하고 다시 한번 [Run → Run As → JUnit Test]을 실행하면 테스트가 성공했다고 표시될 것이다.

우리는 편의상 testJpa 메서드 하나만을 가지고 JPA의 여러 기능을 테스트할 것이다.

findById 메서드

이번에는 질문 엔티티의 기본키인 id의 값을 활용해 데이터를 조회해 보자.

여기서 말하는 '값'은 테이블에 저장된 데이터들을 말한다. 예를 들어 id값은 1 또는 2이다. 이 값을 활용해 조회한다.

테스트 코드를 다음과 같이 수정하자.

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 메서드를 사용해야 한다. 여기서는 questionRepository를 사용하여 데이터베이스에서 id가 1인 질문을 조회한다. 이때 findById의 리턴 타입은 Question이 아닌 Optional임에 주의하자. findById로 호출한 값이 존재할 수도 있고, 존재하지 않을 수도 있어서 리턴 타입으로 Optional이 사용된 것이다. Optional은 그 값을 처리하기 위한(null값을 유연하게 처리하기 위한) 클래스로, isPresent() 메서드로 값이 존재하는지 확인할 수 있다. 만약 isPresent()를 통해 값이 존재한다는 것을 확인했다면, get() 메서드를 통해 실제 Question 객체의 값을 얻는다. 즉, 여기서는 데이터베이스에서 ID가 1인 질문을 검색하고, 이에 해당하는 질문의 제목이 ‘sbb가 무엇인가요?’인 경우에 JUnit 테스트를 통과하게 된다.

findBySubject 메서드

이번에는 질문 엔티티의 subject값으로 데이터를 조회해 보자.

1) 아쉽게도 리포지터리는 findBySubject 메서드를 기본적으로 제공하지는 않는다. 그래서 findBySubject 메서드를 사용하려면 다음과 같이 QuestionRepository 인터페이스를 변경해야 한다. 먼저 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);
}

2) 다시 src/test/java 디렉터리로 돌아가 com.mysite.sbb 패키지의 SbbApplicationTests.java를 수정해 subject 값으로 테이블에 저장된 데이터를 조회할 수 있다.

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라는 메서드를 선언만 하고 구현하지 않았는데 도대체 어떻게 실행되는 거지?’라는 궁금증이 생길 수 있다. 이러한 마법은 JPA에 리포지터리의 메서드명을 분석하여 쿼리를 만들고 실행하는 기능이 있기 때문에 가능하다. 즉, 여러분은 findBy + 엔티티의 속성명(예를 들어 findBySubject)과 같은 리포지터리의 메서드를 작성하면 입력한 속성의 값으로 데이터를 조회할 수 있다!

3) findBySubject 메서드를 호출할 때 실제 데이터베이스에서는 어떤 쿼리문이 실행되는지 살펴보자. 실행되는 쿼리문은 콘솔(console) 로그에서 확인할 수 있다. 그러기 위해 다음과 같이 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

4) 그리고 다시 한번 테스트 코드를 실행해 보자. 그러면 다음과 같이 콘솔 로그에서 데이터베이스에서 실행된 쿼리문를 확인할 수 있다.

실행된 쿼리문 중 where 문에 조건으로 subject가 포함된 것을 확인할 수 있다.

점프 투 스프링부트WHERE 문으로 조건에 맞는 데이터를 검색하자

SQL에서는 원하는 조건에 맞는 검색을 할 때 WHERE 문을 사용한다. 기본 형식은 다음과 같다.

SELETE 열 FROM 테이블 WHERE 열 = 조건값   

이때 WHERE 문의 '열'에는 검색할 열을 입력하고, 조건값으로는 사용자가 찾을 데이터값을 넣는다. 그리고 연산자는 다양하지만 여기서는 열에 있는 데이터값과 조건값이 일치하는 것을 검색하기 위해 '=' 기호를 사용했다.

findBySubjectAndContent 메서드

1) 이번에는 subject와 content를 함께 조회해 보자. SQL을 활용해 데이터베이스에서 두 개의 열(여기서는 엔티티의 속성)을 조회하기 위해서는 And 연산자를 사용한다. 우리는 subject와 content 속성을 조회하기 위해 findBySubject와 마찬가지로 먼저 리포지터리에 findBySubjectAndContent 메서드를 추가해야 한다. 다음과 같이 QuestionRepository.java 파일을 수정해 보자.

SQL에서 And 연산자를 활용하면 여러 조건을 결합해 데이터를 조회할 수 있다.

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);
}

2) 그리고 테스트 코드를 다음과 같이 작성하자.

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());
    }
}

3) 테스트는 잘 통과될 것이다. 그리고 다음과 같이 콘솔 로그에서 데이터베이스에서 실행된 쿼리문을 확인할 수 있다.

where 문에 and 연산자가 사용되어 subject와 content 열을 조회하는 것을 확인할 수 있다.

이렇듯 리포지터리의 메서드명은 데이터를 조회하는 쿼리문의 where 조건을 결정하는 역할을 한다. 여기서는 findBySubject, findBySubjectAndContent 두 메서드만 알아봤지만 상당히 많은 조합을 사용할 수 있다. 조합할 수 있는 메서드를 간단하게 표로 정리해 보았다. 한번 살펴보자.

SQL 연산자리포지터리의 메서드 예시설명

And findBySubjectAndContent(String subject, String content) Subject, Content 열과 일치하는 데이터를 조회
Or findBySubjectOrContent(String subject, String content) Subject열 또는 Content 열과 일치하는 데이터를 조회
Between findByCreateDateBetween(LocalDateTime fromDate, LocalDateTime toDate) CreateDate 열의 데이터 중 정해진 범위 내에 있는 데이터를 조회
LessThan findByIdLessThan(Integer id) Id 열에서 조건보다 작은 데이터를 조회
GreaterThanEqual findByIdGreaterThanEqual(Integer id) Id 열에서 조건보다 크거나 같은 데이터를 조회
Like findBySubjectLike(String subject) Subject 열에서 문자열 ‘subject’와 같은 문자열을 포함한 데이터를 조회
In findBySubjectIn(String[] subjects) Subject 열의 데이터가 주어진 배열에 포함되는 데이터만 조회
OrderBy findBySubjectOrderByCreateDateAsc(String subject) Subject 열 중 조건에 일치하는 데이터를 조회하여 CreateDate 열을 오름차순으로 정렬하여 반환

쿼리와 관련된 JPA의 메서드를 자세히 알고 싶다면 쿼리 생성 규칙을 담은 다음의 스프링 공식 문서를 참고하자. https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation

응답 결과가 여러 건인 경우에는 리포지터리 메서드의 리턴 타입을 Question이 아닌 List<Question>으로 작성해야 함을 꼭 기억해 두자.

findBySubjectLike 메서드

1) 이번에는 질문 엔티티의 subject 열 값들 중에 특정 문자열을 포함하는 데이터를 조회해 보자. SQL에서는 특정 문자열을 포함한 데이터를 열에서 찾을 때 Like를 사용한다. subject 열에서 특정 문자열을 포함하는 데이터를 찾기 위해 다음과 같이 findBySubjectLike 메서드를 리포지터리에 추가해 보자.

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);
}

2) 그리고 테스트 코드는 다음과 같이 수정하자.

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());
    }
}

테스트는 잘 통과될 것이다. findBySubjectLike 메서드를 사용할 때 데이터 조회를 위한 조건이 되는 문자열로 sbb%와 같이 %를 적어 주어야 한다. %는 표기하는 위치에 따라 의미가 달라진다. 다음 표를 살펴보자.

표기 예표기 위치에 따른 의미

sbb% 'sbb'로 시작하는 문자열
%sbb 'sbb'로 끝나는 문자열
%sbb% 'sbb'를 포함하는 문자열

질문 데이터 수정하기

1) 이번에는 질문 엔티티의 데이터를 수정하는 테스트 코드를 작성해 보자.

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(참) 인지를 테스트한다. oq.isPresent()가 false를 리턴하면 오류가 발생하고 테스트가 종료된다.

질문 엔티티의 데이터를 조회한 다음, subject 속성을 '수정된 제목'이라는 값으로 수정했다. 변경된 질문을 데이터베이스에 저장하기 위해서 this.questionRepository.save(q)와 같이 리포지터리의 save 메서드를 사용했다.

2) 테스트를 수행해 보면 다음과 같이 콘솔 로그에서 update 문이 실행되었음을 확인할 수 있을 것이다.

update
  question 
set
  content=?,
  create_date=?,
  subject=? 
where
  id=?

그리고 H2 콘솔에 접속하여 SELECT * FROM QUESTION 쿼리문을 입력하고 실행해 question 테이블을 확인하면 다음과 같이 subject의 값이 변경되었음을 알 수 있다.

질문 데이터 삭제하기

1) 이어서 데이터를 삭제해 보자. 여기서는 첫 번째 질문을 삭제해 보자.

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 메서드는 테이블 행의 개수를 리턴한다.

리포지터리의 delete 메서드를 사용하여 데이터를 삭제했다. 데이터 건수가 삭제하기 전에 2였는데, 삭제한 후 1이 되었는지를 테스트했다. 테스트는 잘 통과될 것이다.

2) 그리고 다시 question 테이블을 확인해 보면 다음과 같이 ID가 1인 행이 삭제되었음을 알 수 있다.

답변 데이터 저장하기

1) 이번에는 답변 엔티티의 데이터를 생성하고 저장해 보자. 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)가 필요하므로 AnswerRepository의 객체를 @Autowired를 통해 주입했다. 답변을 생성하려면 질문이 필요하므로 우선 질문을 조회해야 한다. questionRepository의 findById 메서드를 통해 id가 2인 질문 데이터를 가져와 답변의 question 속성에 대입해 답변 데이터를 생성했다. 테스트를 수행하면 오류 없이 답변 데이터가 잘 생성될 것이다.

참고로 ‘질문 데이터 삭제하기’ 실습에서 question 테이블에 id가 2인 행만 남겼었다.

2) 데이터베이스에 값이 잘 들어갔는지 확인해 보기 위해 H2 콘솔에 접속하여 다음 쿼리문을 실행해 보자.

SELECT * FROM ANSWER

그러면 다음과 같이 우리가 작성한 대로 데이터가 저장된 것을 확인할 수 있다.

답변 데이터 조회하기

답변 엔티티도 질문 엔티티와 마찬가지로 id 속성이 기본키이므로 값이 자동으로 생성된다. 질문 데이터를 조회할 때 findById 메서드를 사용했듯이 id값을 활용해 데이터를 조회해 보자.

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 질문 데이터를 통해 답변 데이터 찾기

앞에서 살펴본 답변 엔티티의 question 속성을 이용하면 다음과 같은 메서드를 사용해 '답변에 연결된 질문'에 접근할 수 있다.

a.getQuestion()

a는 답변 객체이고, a.getQuestion()은 답변에 연결된 질문 객체를 뜻한다.

답변에 연결된 질문 데이터를 찾는 것은 Answer 엔티티에 question 속성이 이미 정의되어 있어서 매우 쉽다.

그런데 반대의 경우도 가능할까? 즉, 질문 데이터에서 답변 데이터를 찾을 수 있을까? 다음과 같이 질문 엔티티에 정의한 answerList를 사용하면 해결할 수 있다.

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인 질문 데이터에 답변 데이터를 1개 등록했으므로 이와 같이 코드를 작성해 확인할 수 있다.

그런데 이 코드를 실행하면 표시한 위치에 다음과 같은 오류가 발생한다.

왜냐하면 QuestionRepository가 findById 메서드를 통해 Question 객체를 조회하고 나면 DB 세션이 끊어지기 때문이다.

DB 세션이란 스프링 부트 애플리케이션과 데이터베이스 간의 연결을 뜻한다.

그래서 그 이후에 실행되는 q.getAnswerList() 메서드(Question 객체로부터 answer 리스트를 구하는 메서드)는 세션이 종료되어 오류가 발생한다. answerList는 앞서 q 객체를 조회할 때가 아니라 q.getAnswerList() 메서드를 호출하는 시점에 가져오기 때문에 이와 같이 오류가 발생한 것이다.

이렇게 데이터를 필요한 시점에 가져오는 방식을 지연(Lazy) 방식이라고 한다. 이와 반대로 q 객체를 조회할 때 미리 answer 리스트를 모두 가져오는 방식은 즉시(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());
    }
}

메서드에 @Transactional 애너테이션을 추가하면 오류 없이 잘 수행될 것이다. 다시 한번 확인해 보자.

 

참고링크:https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html

 

JPA Query Methods :: Spring Data JPA

By default, Spring Data JPA uses position-based parameter binding, as described in all the preceding examples. This makes query methods a little error-prone when refactoring regarding the parameter position. To solve this issue, you can use @Param annotati

docs.spring.io

https://wikidocs.net/160890