[스프링부트 시리즈8] 리포지토리로 DB관리하기

데이터 관리용 JPA 리포지토리 만들기.

Featured image

리포지토리로 DB 관리하기


엔티티만으로는 테이블의 데이터를 조회나 저장, 수정을 할 수 없다. 데이터를 관리하려면 데이터베이스와 연동되는 JPA 리포지토리가 반드시 필요하다.

리포지토리 생성하기


리포지토리는 엔티티가 할 수 없는 생성된 데이터베이스 테이블 데이터들을 수정, 저장, 조회, 삭제 할 수 있게 해주는 인터페이스다. 리포지토리는 테이블에 접근해, 데이터를 관리하는 메서드들을 제공한다.

1) 리포지토리를 만들려면 com.mysite.sbb패키지에서 New -> Interface를 클릭해 QuestionRespository 아래처럼 인터페이스를 생성하자.

  package com.mysite.sbb;
  import org.springframework.data.jpa.repository.JpaRepository;
  public interface QuestionRepository extends JpaRepository<Question, Integer>{

  }

생성한 QuestionRepository 인터페이스를 리포지토리로 만들려면 JpaRepository 인터페이스를 상속한다. JpaRepository는 JPA가 제공하는 인터페이스 중 하나로서 CRUD 작업을 처리하는 메서드들을 내장하여, DB 관리작업을 수월하게 해준다. JpaRepository<Question, Integer>는 Question 엔티티로 리포지토리를 생성한다는 의미이다. Question 엔티티의 기본키가 Integer임을 추가로 지정해야 한다.

CRUD는 Create,Read,Update,Delete의 약자로 데이터처리의 기본기능을 말한다.

2) AnswerRepository 인터페이스도 생성한다.

  package com.mysite.sbb;
  import org.springframework.data.jpa.repository.JpaRepository;
  public interface AnswerRepository extends JpaRepository<Answer, Integer>{

  }

이제 QuestionRepository, AnswerRepository를 이용해 question, answer 테이블의 데이터를 CRUD할 수 있게 되었다.

JUnit 설치하기


리포지토리를 써서 데이터를 저장하려면 질문을 등록하는 화면과 사용자가 입력한 질문 관련 정보를 저장하는 컨트롤러, 서비스 파일등이 필요하다. 하지만 JUnit을 쓴다면 이런 프로세스가 필요하지 않고, 리포지토리만 개별 실행하여 테스트 할 수 있다. 앞서 작성한 리포지토리가 정상 작동하는지 직접 테스트하기 위해 먼저 JUnit을 설치하자.

  • JUnit은 코드를 작성하고 작성한 코드를 테스트할 때 사용하는 자바 테스트프레임워크다. 소프트웨어 개발 시 테스트용으로 자주 사용한다.

JUnit을 사용하려면 build.gralde.kts파일에 아래와 같이 추가한다.

(... 생략 ...)

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' 
    testImplementation 'org.junit.jupiter:junit-jupiter' 
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

(... 생략 ...)

testRuntimeOnly: 해당 라이브러리가 테스트 실행용으로만 사용됨을 의미한다.

build.gradle.kts 수정했으면 전에 했던것처럼 우측 그레이들패널에서 Reload All Gradle Project를 실행하면 설치가 완료된다.

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 객체를 주입했다.

의존성주입(Dependency Injection)은 스프링이 객체를 대신 생성주입한다.

NOTE
@Autowired 에너테이션? 앞서 작성한 테스트 코드 중 questionRepository 변수는 선언만 있고 값이 없다. 하지만 @Autowired 애너테이션을 해당 변수에 적용하면 스프링부트가 questionRepository 객체를 자동으로 만들어 주입한다. Setter메서드나 생성자를 사용해 주입할 수도 있다. 순환 참조 등의 문제로 인해 @Autowired 보다 생성자를 통한 객체 주입을 권장한다.

WARNING
테스트 코드의 경우 JUnit 이 생성자를 통한 객체 주입을 지원하지 않아 테스트용으로만 @Autowired를 사용하자.

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

testJpa 메서드의 내용을 자세히 살펴보면 testJpa 메서드는 q1, q2라는 질문 엔티티의 객체를 생성하고 QuestionRepository를 이용하여 그 값을 데이터베이스에 저장한다. 이와 같이 데이터를 저장하면 H2 데이터베이스의 question 테이블은 다음과 같은 형태로 저장될 것이다. | ID | Content | CreateDate | Subject | 1 | sbb에 대해 알고싶습니다. | 2024-01-16-01:00:00 | sbb가 뭔가요? | 2 | id는 자동 생성입니까? | 2024-01-16-01:00:00 | 스프링부트 모델 질문입니다.

3) 이제 작성한 SbbApplicationTests 클래스를 실행해 보자. SbbApplication 을 오른 클릭 -> Run을 클릭하면 SbbApplicationTests 클래스를 실행할 수 있다.

TIP 단축키 : ctrl + shift + f10 런

WARNING
만약 로컬 서버가 구동중이라면 오류가 발생할 수 있다. H2 데이터베이스는 파일 기반 데이터베이스인데 이미 로컬 서버가 동일한 DB파일인 (local.mv.db)를 점유중이기 때문에 발생한다. 따라서 테스트 실행전 실행중인 로컬 서버를 중지해야 한다.

4) 오류가 발생했다면 로컬 서버를 중지한 후 테스트를 재실행한다. 테스트에 성공했다면 아래와 같은 메시지가 출력된다. 메시지

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

SELECT * FROM QUESTION 

NOTE
위의 쿼리문은 question 테이블의 모든 행을 조회하는 의미다.

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

id는 question 엔티티의 기본키로서 `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 테이블에 저장된 모든 데이터를 조회하기 위해서 리포지토리의 findAll메서드를 사용한다.

findAll 메서드는 H2 콘솔에서 입력해본 SELECT * FROM QUESTION 하고 같은 결과를 얻게 된다.

앞서 2개의 질문 데이터를 저장해씅니 데이터 사이즈는 2가 되어야한다. 데이터 사이즈가 실제로 2인지 확인하려면 JUnit의 assertEquals메서드를 사용하는데, 이 메서드는 테스트 예상 값과 실제 값이 동일한지 확인하는 목적으로 사용한다. JPA 또는 DB에서 데이터를 올바르게 가져오는지 확인하는 것이다. assertEquals(기대값, 실제값) 과 같이 작성하고 기대값이 실제값이 동일하진 조사해 동일하지 않은 경우 테스트는 실패로 처리된다. 아까 저장한 1번째 데이터의 제목이 ‘ssb가 뭔가요?’ 데이터와 일치하는지도 테스트했다.

WARNING
테스트할때 로컬 서버를 중지하고 JUnit 을 실행한다.

findByld 메서드


이번에는 question 엔티티의 기본키인 id 값을 활용해 데이터를 조회한다.

TIP
여기서 ‘값’ 이란 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 메서드


이번에는 question 엔티티의 subject 값으로 조회해보자. 1) 리포지토리가 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 + 엔티티 속성명(ex.findBySubject) 같은 리포지토리 메서드를 작성하면 입력한 속성값으로 데이터를 조회할 수 있다.

3) finBySubject 메서드를 호출할 때 실제 데이터베이스에서 어떤 쿼리문이 실행되는지 알아보려면 콘솔에서 볼수 있다. 아래와 같이 application.prorperties 파일을 수정한다.

  # 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가 포함되어 있는 것을 확인할 수 있다.

findBySubjectAndContent 메서드

1) subjectcontent 를 함께 조회하려면? SQL을 사용해서 데이터베이스에서 2개의 열(엔티티 속성)을 조회하려면 And 연산자가 필요하다. subjectcontent속성을 조회하려면 findBySubject와 마찬가지로 리포지토리에 findBySubjectAndContent 메서드를 추가해야 한다. 아래와 같이 QuestionRepository.java 파일을 수정한다.

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

테스트 코드는 다음과 같다.

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 연산자가 사용되어 subjectcontent 열을 조회한 것을 확인할 수 있다.

리포지토리의 메서드명은 데이터를 조회하는 쿼리문의 where 조건을 결정한다. findBySubject, findBySubjectAndContent 외에도 상당히 많은 조합을 사용할 수 있다.

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

쿼리 JPA 메서드 생성 규칙 스프링 문서 :
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation

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’를 포함하는 문자열

질문 데이터 수정하기


질문 엔티티의 데이터를 수정하는 테스트 코드를 작성한다.

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) 테스트를 수행하면 콘솔로그에 다음과 같이 뜬다. 콘솔

H2 콘솔에 접속해 SELECT * FROM QUESTION 쿼리문을 입력 실행하면 question 테이블에 subject 값이 변경된 것이 나온다. 제목

질문 데이터 삭제하기


1) 이어서 1번째 질문을 삭제한다.

제목 여기서 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인 행이 삭제됨을 알수 있다. 삭제

답변 데이터 저장하기


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

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.question()

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

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

이 코드를 실행하면 오류가 발생한다. 오류

QuestionRepositoryfindById 메서드를 통해 Question 객체를 조회하면 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 애너테이션 추가 시 제대로 작동한다.