티스토리 뷰

2-04에서 엔티티로 테이블을 구성하였다. 

 

엔티티는 데이터베이스 테이블을 생성하게 하는 역할이고, 

리포지터리는 데이터베이스 테이블의 데이터들을 저장, 조회, 수정, 삭제 등을 할 수 있게 도와주는 인터페이스이다.

이때 리포지터리는 테이블에 접근하고, 데이터를 관리하는 메소드 (findAll, save 등) 을 제공한다.

 

리포지터리 생성하기

QuestionRepository 라는 리포지터리를 생성할 때는 아래처럼 작성한다.

QuestionRepository 는 interface 로 생성해야 하고, JpaRepository 를 상속해야 (extends) 한다.

 

아래와 같이 작성하면, JpaRepository 가 제공하는 메소드를 쓸 수 있다.

JpaRepository 가 제공하는 기본 메소드 외에도, 사용자가 직접 메소드를 추가하여 쓸 수 있다.

public interface QuestionRepository extends JpaRepository<Question, Integer>{

}

 

JpaRepository<Question, Integer> 는

Question 엔티티로 리포지터리를 생성한다는 의미이다.

또한 Question 엔티티의 기본키가 Integer 임을 의미한다. 

 

JpaRepository 를 상속할 때는

JpaRepository <대상 엔티티, 대상 엔티티의 기본키 타입>  으로 쓰는 것이 규칙이다.

 

JUnit 설치하기

리포지터리를 이용하여 데이터를 저장하려면, 

질문을 등록하는 화면, 사용자가 입력한 질문 관련 정보를 저장하는 컨트롤러, 서비스 파일 등이 필요하다.

하지만 JUnit을 사용하면 이러한 프로세스를 따르지 않고도 리포지터리만 개별적으로 실행해 테스트 할 수 있다.

 

Junit 설치는 build.gradle 파일에 아래와 같이 두 줄을 추가한다.

dependencies {
	testImplementation 'org.junit.jupiter:junit-jupiter' 
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

 

 

 

※ 맨 처음에 Spring Starter Project 로 프로젝트를 생성했을 때 Spring Web 디펜던시만 추가하여 생성했었다.

그 당시 최초 build.gradle 파일에는 아래와 같이 작성되어 있었다.

내가 생성한 프로젝트의 build.gradle 파일에는 이미 testRuntimeOnly 'org.junit.platform:junit-platform-launcher' 가 작성된 채로 생성된 것이다!!

그래서 1)의 과정은 생략하고 JUnit을 테스트했더니 문제 없이 잘 되었다.

책에서 추가하라고 했던 testImplementation 'org.junit.jupiter:junit-jupiter' 을 추가하지 않았는데도 JUnit이 잘 동작했다.

아마도 자바 버전에 따른 차이점 같은 게 아닐까 하고 추측해봄.... (혹시 아시는 분 댓글 달아주시면 정말 감사하겠습니다.)

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

 

--> 내 프로젝트의 순정 build.gradle 파일 내용..

 

 

JUnit 으로 JpaRepository 테스트 하기

src/test/java 밑에 com.study.board 밑에 BoardApplicationsTest.java 파일을 연다.

package com.study.board;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.time.LocalDateTime;
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 com.study.board.question.Question;
import com.study.board.question.QuestionRepository;
import com.study.board.question.QuestionService;

@SpringBootTest
class BoardApplicationTests {
	
    //스프링의 DI 기능으로 의존성주입
    @Autowired
    private QuestionRepository questionRepository;

    /* 서비스단까지 연계를 하고 나서 테스트 데이터를 300건 넣어주기 위해 주입한 내용이다.
    @Autowired
    private QuestionService questionService;
    */

    @Test
    void testJpa() {

        // 1. 데이터 save 테스트
        Question q1 = new Question();
        q1.setSubject("보드게시판의 제목입니다.");
        q1.setContent("게시판의 질문 내용입니다..?");
        q1.setWriteDate(LocalDateTime.now());
        this.questionRepository.save(q1);	//첫번째 질문 저장

        Question q2 = new Question();
        q2.setSubject("제목2");
        q2.setContent("내용2");
        q2.setWriteDate(LocalDateTime.now());
        this.questionRepository.save(q2);	//두번째 질문 저장


        // 2. 데이터 findAll() 테스트
        List<Question> all = this.questionRepository.findAll();
        assertEquals(2, all.size());		//assertEquals : 검증함수, 매개변수의 두 값이 같은지 비교

        Question q = all.get(0);		// index=0인 객체. 즉, id=1인 데이터 
        assertEquals("보드게시판의 제목입니다.", q.getSubject());


        // 3. id에 해당하는 Question 데이터 조회하는 쿼리
        Optional<Question> oq = this.questionRepository.findById(1);	//id=1인 데이터 조회
        if(oq.isPresent()) {
        Question q = oq.get();
        assertEquals("보드게시판의 제목입니다.", q.getSubject());
        }

        // 4. findBySubject 라는 메소드를 만들고, subject 값이 정확히 일치하는 데이터 조회
        Question q = this.questionRepository.findBySubject("제목2");
        assertEquals(2, q.getId());

        // 5. findBySubjectAndContent 라는 메소드를 만들고, subject와 content 값이 정확히 일치하는 데이터 조회
        // 이때 메소드명 작성 시, SubjectAndContent 라고 지어주어야 한다. (and 조건)
        Question q = this.questionRepository.findBySubjectAndContent("제목2", "내용2");
        System.out.println(q.getSubject());

        // 6. subject가 '제목'으로 시작하는 데이터를 조회한다. (like 조건)
        // 메소드명 작성 시, SubjectLike 라고 지어주어야 한다.
        List<Question> qList = this.questionRepository.findBySubjectLike("제목%");
        Question q = qList.get(0);
        System.out.println(q.getSubject());

        //7. update 테스트
        Optional<Question> oq = this.questionRepository.findById(1);
        assertTrue(oq.isPresent());
        Question q = oq.get();
        q.setSubject("수정된 제목");
        this.questionRepository.save(q);

        // 8. delete 테스트
        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());



		/*
        서비스단까지 연계를 하고 나서 작성한 내용으로,
        Repository 가 아닌 Service 에 접근하여 테스트 데이터를 넣은 내용이다.

        //대량테스트데이터 insert
        for(int i=1; i<=300; i++) {
            String subject = String.format("테스트 데이터입니다:[%03d]", i);
            String content = "내용무";
            this.questionService.create(subject, content, null);
        }
        */
        
	}

}

 

@SpringBootTest 어노테이션 : boardApplicationTests 클래스가 스프링 부트의 테스트 클래스 임을 의미한다.

@Autowired 어노테이션 : 해당 어노테이션을 통해 스프링의 '의존성 주입(DI)' 기능을 사용하여 QuestionRepository 객체를 주입했다.

<@Autowired 어노테이션에 대한 추가 설명>
questionRepository 변수는 선언만 되어 있고 그 값이 비어있다.
해당 변수에 @Autowired 를 사용하면 스프링부트가 questionRepository 객체를 자동으로 만들어 주입한다.

Autowired 어노테이션을 사용한 주입 외에도
Setter 메서드 주입 방식, 생성자 주입 방식이 있다. 

순환 참조 문제가 발생할 수 있어서 @Autowired 보다는 생성자 객체 주입 방식을 권장한다.
하지만 테스트 코드의 경우, JUnit 이 생성자 객체 주입을 지원하지 않고 있으므로, 
테스트 코드 작성시에는 @Autowired를 사용하고 실제 코드에서는 생성자 객체 주입을 사용하자.

 

@Test 어노테이션 : testJpa 메소드가 테스트 메소드임을 나타낸다. BoardApplicationTests 를 JUnit 으로 실행하면 @Test 어노테이션이 붙은 testJpa 메소드가 실행된다.

 

 

JpaRepository 가 기본적으로 제공하는 메서드

1. 의 save()

2. 의 findAll()

3. 의 findById(Integer id)

 

// 3. id에 해당하는 Question 데이터 조회하는 쿼리
Optional<Question> oq = this.questionRepository.findById(1);	//id=1인 데이터 조회
if(oq.isPresent()) {
    Question q = oq.get();
    assertEquals("보드게시판의 제목입니다.", q.getSubject());
}

 

findById 메서드의 리턴 타입은 Question 이 아닌 Optional 임에 주의한다.

findById 로 호출한 값이 존재할 수도 있고, 존재하지 않을 수도 있기 때문에 리턴 타입으로 Optional 이 사용된 것이다.

Optional 은 null 값을 유연하게 처리하기 위한 클래스로, isPresent() 메서드로 값이 존재하는지 확인할 수 있다.

isPresent() 가 true 라면, get() 메서드를 통해 실제 Question 객체의 값을 얻을 수 있다.

 

 

questionRepository 인터페이스에 직접 작성하여 사용한 메서드

4. 의 findBySubject

5. 의 findBySubjectAndContent 

6. 의 findBySubjectLike

 

QuestionRepository 에 아래와 같이 3개의 메서드를 추가해줬다.

package com.study.board.question;

import java.util.List;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;


public interface QuestionRepository extends JpaRepository<Question, Integer>{
	
    Question findBySubject(String subject);

    Question findBySubjectAndContent(String subject, String content);

    List<Question> findBySubjectLike(String subject);
    
}

 

JPA에는 리포지터리의 메서드명을 분석하여 쿼리를 만들고 실행하는 기능이 있기 때문에,

메소드명을 작성 규칙대로 잘 작성해줘야 오류가 나지 않고 잘 실행된다.

메서드를 "findBy + 엔티티의 속성명" 으로 작성하면, 입력한 속성의 값으로 데이터를 조회할 수 있다.

 

findBySubject(String A) : subject 가 입력한 매개변수 A와 동일한 데이터 조회

--> SELECT * FROM 테이블명 WHERE SUBJECT = 'A';

 

findBySubjectAndContent(String A, String B) : subject, content 가 입력한 매개변수 A, B와 동일한 데이터 조회

--> SELECT * FROM 테이블명 WHERE SUBJECT = 'A' AND CONTENT = 'B';

 

findBySubjectLike(String A) : subject 가 입력한 매개변수 A 로 시작하는 데이터 조회

--> SELECT * FROM 테이블명 WHERE SUBJECT LIKE 'A';

** 문자열 A의 맨 마지막에는 % 가 포함되어야 한다.

** 예를 들어  제목 으로 시작하는 게시글을 조회하고 싶다면, 문자열 A의 자리에  제목% 을 넘겨야 한다.

** %가 위치한 자리에 따라 의미가 달라진다. 다음 표를 확인해보자.

표기 예 표기 위치에 따른 의미
sbb% 'sbb'로 시작하는 문자열
%sbb 'sbb'로 끝나는 문자열
%sbb% 'sbb'를 포함하는 문자열 

 

 

 

실제 데이터베이스에 어떤 쿼리문이 실행되는지 궁금하다면,

콘솔창에 쿼리가 뜨도록 세팅하면 된다.

application.properties 에 아래 두 줄을 추가하면 된다.

#JPA쿼리표출
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.show_sql=true

 

 

리포지터리 메서드명 규칙 표

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>으로 작성해야 함을 꼭 기억하자. 

 

 

 

QuestionRepository 의 내용 중 7, 8번도 확인해보자.

    //7. update 테스트
    Optional<Question> oq = this.questionRepository.findById(1);
    assertTrue(oq.isPresent());
    Question q = oq.get();
    q.setSubject("수정된 제목");
    this.questionRepository.save(q);

    // 8. delete 테스트
    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. update 테스트 

- assertTure() 메서드 : 괄호 안의 값이 true 인지 테스트한다. 만약 false 이면 오류가 발생하고 테스트가 종료된다. (그 밑으로는 실행하지 않음) 

- save() 메서드는 데이터를 신규 insert 하거나, 기존 데이터 udate 할 때 모두 사용된다.

insert를 할 때는 Question q = new Question(); 을 한 후, q에 값을 set 한 후, save(q) 해줬다면,

update를 할 때는 id 값으로 q를 찾아온 후, 조회된 데이터 q에 set 하고 save(q) 한다는 점이다.

 

8. delete 테스트

- id로 데이터를 찾아온 후, 해당 객체 q를 delete(q) 하면 된다.

 

 

Answer 데이터 테스트 해보기

앞전에 엔티티를 작성하면서 하나의 Question 안에 여러개의 Answer이 달려있는 구조로 설계하였다.

그리고 각 Answer 들은 어느 Question 에 달려 있는 글인지를 알기 위해서 Anwer 테이블 안에 question_id 가 생성될 수 있게 설계하였다.

 

아래와 같이 작성 후 테스트 해보았다.

package com.study.board;

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;

import com.study.board.question.Question;
import com.study.board.question.QuestionRepository;
import com.study.board.answer.AnswerRepository;

@SpringBootTest
class SbbApplicationTests {

    @Autowired
    private QuestionRepository questionRepository;

    @Autowired
    private AnswerRepository answerRepository;

    @Test
    void testJpa() {
    
        //1. Answer 데이터 insert
        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.setWriteDate(LocalDateTime.now());
        this.answerRepository.save(a);


        //2. Answer 데이터 조회
        Optional<Answer> oa = this.answerRepository.findById(1);
        assertTrue(oa.isPresent());
        Answer a = oa.get();
        assertEquals(2, a.getQuestion().getId()); // answer 객체를 통해 question 의 id 조회
    }
}

 

1. Answer 데이터 insert

먼저 id가 2인 question 객체를 가져왔다. 

그 다음 Answer 을 new 해서 answer 객체를 생성해 주었다.

aswer 에 setContent , setWriteDate 하여 세팅해 주었고,

answer 객체에 setQuestion(q)를 해서 question 객체를 넘겨주었다.

 

** 앞전에 Answer 엔티티에서 Question 엔티티와 N:1 관계를 나타내주기 위해서 

@ManyToOne

private Quesetion question; 

설정을 해주었다.

그렇기 때문에 a.setQuestion(q); 를 할 수 있는 것이다.

그리고 이러한 연결 관계가 생겼기 때문에 answer 객체에 question 객체를 넘겨서 insert 하면

ANSWER 테이블에 생성된 QUESTION_ID 컬럼에 넘겨준 question 의 id 가 들어간다.

 

2. Answer 데이터 조회

id 가 1인 answer 객체를 조회하고, 

a.getQuestion().getId() 를 하여 ANSWER 의 QUESTION_ID 값을 가져와서 assertEquals로 비교한다.

 

 

 

답변 데이터를 통해 질문 데이터 찾기 vs 질문 데이터를 통해 답변 데이터 찾기 (지연방식과 즉시방식 비교)

 

Answer 엔티티의 question 속성을 이용하면 

a.getQuestion() 메서드를 사용하여 '답변에 연결된 질문'에 접근할 수 있다.

 

이렇게 답변에 연결된 질문 데이터를 찾을 수 있었던 이유는 

Answer 엔티티에 question 속성을 이미 정의해두었기 때문이다. 

 

그렇다면 반대의 경우도 가능할까? 즉 질문 데이터에서 답변 데이터를 찾을 수 있을까?

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인 Question 객체 q를 조회하고,

q.getAnswerList() 하여  질문에 달린 답변 전체를 구하는 코드이다.

 

JUnit 을 실행하면 오류가 발생한다.

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

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

 

그래서 그 이후 실행되는 q.getAnswerList() 메서드는 세션이 종료되어 오류가 발생한다.

answerList 는 앞서 q 객체를 조회할 때 가져오는 게 아니라,

q.getAnswerList() 메서드를 호출하는 시점에 가져오기 때문에 이와 같은 오류가 발생한다. 

 

이렇게 데이터를 필요한 시점에 가져오는 방식을 지연(Lazy) 방식이라고 한다.

이와 반대로 q 객체를 조회할 때 미리 answer 리스트를 모두 가져오는 방식은 즉시(Eager) 방식이라고 한다.

@OneToMany, @ManyToOne 어노테이션의 옵션으로 fetch=FetchType.LAZY 또는 fetch=FetchType.EAGER 처럼 설정할 수 있다. 

그런데 이 책의 예제를 따라할 때는 따로 지정하지 않았고 기본값을 사용하게 하였다. (즉, 기본값은 지연 방식이다.)

 

사실 이러한 문제는 테스트 코드에서만 발생한다.

실제 서버에서 JPA 프로그램들을 실행할 때는 DB 세션이 종료되지 않기 때문에 위와 같은 오류가 발생하지 않는다.

 

테스트 코드를 수행할 때 이러한 오류를 방지하기 위해서는,

testJpa() 메서드 단에 @Transactional 어노테이션을 추가하면 된다. 

이 어노테이션을 사용하면 메서드가 종료될 때까지 DB 세션이 유지된다. 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
글 보관함