elevne's Study Note
Spring Boot 복습 (2) 본문
저번 시간에는 JPA, ORM 활용을 위해 엔티티를 생성해두었다. 하지만 Entity 만으로는 데이터베이스에 데이터를 저장하거나 조회할 수 없다. 데이터 처리를 위해서는 실제 DB 와 연동하는 JDBC Repository 가 필요하다. Repository 는 Entity 에 의해 생성된 DB 테이블에 접근하는 메서드들 (e.g., findAll, save 등) 을 사용하기 위한 인터페이스이다. 데이터 처리를 위해서는 테이블에 어떤 값을 넣거나 값을 조회하는 등의 CRUD 가 필요한데, 이 때 이러한 CRUD 를 어덯게 처리할지 정의하는 계층이 바로 Repository 이다.
아래와 같이 Question 을 위한 Repository 코드를 작성한다.
package com.springboot.study.repository;
import com.springboot.study.entity.Question;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Question, Integer> {
}
QuestionRepository 인터페이스는 Repository 로 만들기 위해 JpaRepository 인터페이스를 상속받는다. JpaRepository 를 상속받을 때는 Generics 타입으로 <Question, Integer> 처럼 Repository 의 대상이 되는 Entity 의 타입과 해당 Entity 의 PK 속성 타입을 지정해야 한다. AnswerRepository 도 동일하게 작성한다. 이제 Repository 들을 이용하여 question, answer 테이블에 데이터를 저장, 조회할 수 있다.
작성한 Repository 를 테스트하기 위해 JUnit 기반의 Spring Boot Test 프레임워크를 사용했다. test 디렉토리 밑에 있는 StudyApplicationTest 파일을 열어 아래와 같이 작성해주었다.
package com.springboot.study;
import com.springboot.study.entity.Question;
import com.springboot.study.repository.QuestionRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
@SpringBootTest
class StudyApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJps() {
Question q1 = new Question();
q1.setSubject("TEST SUBJECT");
q1.setContent("TEST CONTENT");
q1.setCreatedDate(LocalDateTime.now());
this.questionRepository.save(q1);
Question q2 = new Question();
q2.setSubject("TEST SUBJECT2");
q2.setContent("TEST CONTENT2");
q2.setCreatedDate(LocalDateTime.now());
this.questionRepository.save(q2);
}
}
@SpringBootTest 어노테이션은 해당 클래스가 테스트 클래스임을 의미한다. @Autowired 어노테이션은 Spring 의 DI(Dependency Injection) 기능으로 questionRepository 객체를 Spring 이 자동으로 생성해주는 역할을 한다. 객체를 주입하는 방식에는 @Autowired 외에도 Setter 을 사용하거나 생성자를 사용할 수 있다. 순환참조와 같은 문제가 있기에 @Autowired 보다는 생성자를 통한 객체 주입방식이 권장되는데, 테스트 코드의 경우에는 생성자를 통한 객체의 주입이 불가능하기 때문에 테스트 작성 시에만 @Autowired 를 사용하고 실제 코드에는 생성자를 통한 객체 주입 방식을 사용한다.
Intellij 기준으로, Current file 을 실행함으로써 Test 를 진행해보면 BUILD SUCCESSFUL 이라고 뜨는 것을 확인할 수 있다.
실제 DB 에도 잘 들어갔는지 console 에 들어가서 확인해보았다.
그 다음으로는 DB 에 저장된 데이터를 조회해보았다. Test 코드는 아래와 같이 작성해주었다.
package com.springboot.study;
import com.springboot.study.entity.Question;
import com.springboot.study.repository.QuestionRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest
class StudyApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJps() {
List<Question> questions = this.questionRepository.findAll();
assertEquals(2, questions.size());
Question q = questions.get(0);
assertEquals(q.getSubject(), "TEST SUBJECT");
}
}
findAll 은 모든 데이터를 조회할 때 사용되는 메서드이다. 총 2 건의 데이터를 저장했기에 데이터의 사이즈는 2 가 되어야 한다. 데이터 사이즈가 2 인지 확인하기 위해 JUnit 의 assertEquals 메서드를 사용한다. assertEquals 는 기대값, 실제값 을 인자로 받아 둘이 동일한지 확인한다. 만약 기대값과 실제값이 다르다면 테스트는 실패처리된다.
findAll 외에도 findById, findBySubject 등으로 데이터를 조회해볼 수도 있다.
package com.springboot.study;
import com.springboot.study.entity.Question;
import com.springboot.study.repository.QuestionRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest
class StudyApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJps() {
Optional<Question> q = this.questionRepository.findById(5);
if (q.isPresent()) {
Question question = q.get();
assertEquals("TEST SUBJECT", question.getSubject());
}
}
}
findById 라는 메서드를 따로 정의해주지는 않았지만 곧바로 사용할 수 있다. 하지만 이 때는 리턴타입이 Question 이 아닌 Optional 이다. Optional 은 null 처리를 유연하게 하기 위해 사용되는 클래스로 위와 같이 isPresent 로 null 아닌 것을 확인한 후에 get 으로 실제 Question 객체를 얻을 수 있다.
Subject 로 조회하기 위해서는 Question Repository 에 따로 메서드를 만들어줄 필요가 있다. Question findBySubject(String subject); 와 같이 작성해준다. 그 다음부터 바로 이 메서드를 사용할 수 있다.
package com.springboot.study;
import com.springboot.study.entity.Question;
import com.springboot.study.repository.QuestionRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest
class StudyApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJps() {
Question q = this.questionRepository.findBySubject("TEST SUBJECT");
assertEquals("TEST SUBJECT", q.getSubject());
}
}
이러한 이름의 find 메서드들은 JpaRepository 를 상속한 QuestionRepository 객체가 생성될 때 같이 생성된다. DI 에 의해 Spring 이 자동으로 QuestionRepository 객체를 생성하고, 이 때 프록시 패턴이 사용된다고 한다. Repository 객체의 메서드가 실행될 때 JPA 가 해당 메서드명을 분석하여 쿼리를 만들고 실행한다. application.properties 파일에 아래 설정을 추가해주면 실행되는 쿼리를 콘솔 창에서 확인해볼 수 있다.
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.show_sql=true
제목과 내용을 동시에 사용하여 조회해볼 수도 있다. 이 때는 And 를 이름에 넣어주면 된다.
package com.springboot.study.repository;
import com.springboot.study.entity.Question;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Question, Integer> {
Question findBySubject(String subject);
Question findBySubjectAndContent(String subject, String content);
}
위와 같은 메서드를 사용하면 subject, content 컬럼이 and 조건으로 where 문에 사용되는 것이다. 이 외에도 아래 표와 같이 많은 조합을 사용해볼 수 있다고 한다.
응답 결과가 여러 건인 경우에는 Repository 메서드의 return 타입을 List 로 해주면 된다.
그 다음으로는 제목 또는 내용에 특정 문자열이 포함되어 있는 데이터를 조회하는 메서드를 아래와 같이 작성해주고, 테스트해보았다.
package com.springboot.study.repository;
import com.springboot.study.entity.Question;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface QuestionRepository extends JpaRepository<Question, Integer> {
Question findBySubject(String subject);
Question findBySubjectAndContent(String subject, String content);
List<Question> findBySubjectLikeOrContentLike(String subject, String content);
}
package com.springboot.study;
import com.springboot.study.entity.Question;
import com.springboot.study.repository.QuestionRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest
class StudyApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
List<Question> questions = this.questionRepository.findBySubjectLikeOrContentLike("%TEST%", "%TEST%");
Question q = questions.get(0);
assertEquals("TEST SUBJECT", q.getSubject());
}
}
위 Test 코드와 같이 LIKE 검색을 위해서는 % 와 같은 와일드카드가 필요하다.
데이터를 수정하는 메서드는 아래와 같이 작성한다.
package com.springboot.study;
import com.springboot.study.entity.Question;
import com.springboot.study.repository.QuestionRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SpringBootTest
class StudyApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
Optional<Question> oq = this.questionRepository.findById(5);
assertTrue(oq.isPresent());
Question question = oq.get();
question.setSubject("TEST SUBJECT EDITED");
this.questionRepository.save(question);
}
}
assertTrue 는 값이 true 인지 검사한다. 질문 데이터를 조회한 뒤 subject 를 수정, save() 메서드를 사용한 결과 update 쿼리가 실행되는 것을 확인할 수 있다. 삭제의 경우에는 save() 메서드 대신에 delete() 로만 교체해주면 된다. (추가로, this.questionRepository.count() 는 데이터 건수를 리턴한다)
그 다음으로는 질문 데이터와 함께 답변 데이터도 생성해서 저장해보았다.
package com.springboot.study;
import com.springboot.study.entity.Answer;
import com.springboot.study.entity.Question;
import com.springboot.study.repository.AnswerRepository;
import com.springboot.study.repository.QuestionRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SpringBootTest
class StudyApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Autowired
private AnswerRepository answerRepository;
@Test
void testJpa() {
Optional<Question> oq = this.questionRepository.findById(5);
assertTrue(oq.isPresent());
Question question = oq.get();
Answer a = new Answer();
a.setContent("TEST ANSWER");
a.setQuestion(question);
a.setCreateDate(LocalDateTime.now());
this.answerRepository.save(a);
}
}
답변 데이터의 처리를 위해서는 질문 데이터가 필요하기에 질문 데이터를 먼저 구하고, setQuestion 을 해준 다음 저장한다.
Answer 엔티티의 Question 속성을 이용하여 답변에 연결된 질문은 a.getQuestion() 메서드로 조회할 수 있다. 또한, 답변에 연결된 질문도 Answer 엔티티에 getAnswerList 를 사용하여 구할 수 있다.
package com.springboot.study;
import com.springboot.study.entity.Answer;
import com.springboot.study.entity.Question;
import com.springboot.study.repository.AnswerRepository;
import com.springboot.study.repository.QuestionRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SpringBootTest
class StudyApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Autowired
private AnswerRepository answerRepository;
@Test
void testJpa() {
Optional<Question> oq = this.questionRepository.findById(5);
assertTrue(oq.isPresent());
Question question = oq.get();
List<Answer> answerList = question.getAnswerList();
assertEquals(1, answerList.size());
}
}
위 테스트 코드는 실패한다. Question Repository 가 findById 를 호출하여 Question 객체를 조회하고 나면 DB 세션이 끊어지기 때문이다. 그 이후에 실행되는 q.getAnswerList() 메서드는 세션이 종료되어 오류가 발생한다. 답변 데이터 리스트는 q 객체를 조회할 때 가져오지 않고 q.getAnswerList() 메서드를 호출하는 시점에 가져오기 때문이다. (LAZY 방식으로 기본으로 사용하고 있는 것)
그런데 사실 이러한 문제는 Test 코드에서만 발생한다고 한다. 실제 프로그램을 실행할 때는 DB 세션이 종료되지 않기 때문에 위와 같은 오류가 발생하지 않는다. 테스트를 실행할 때 위 에러를 방지하는 방법은 @Transactional 어노테이션을 사용하는 것이라고 한다. 이를 사용하면 메서드가 종료될 때까지 DB 세션이 유지된다고 한다.
package com.springboot.study;
import com.springboot.study.entity.Answer;
import com.springboot.study.entity.Question;
import com.springboot.study.repository.AnswerRepository;
import com.springboot.study.repository.QuestionRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import javax.transaction.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SpringBootTest
class StudyApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Autowired
private AnswerRepository answerRepository;
@Test
@Transactional
void testJpa() {
Optional<Question> oq = this.questionRepository.findById(5);
assertTrue(oq.isPresent());
Question question = oq.get();
List<Answer> answerList = question.getAnswerList();
assertEquals(1, answerList.size());
}
}
Reference:
'Backend > Spring' 카테고리의 다른 글
Spring Boot 복습 (5) (0) | 2023.04.09 |
---|---|
Spring Boot 복습 (4) (0) | 2023.04.08 |
Spring Boot 복습 (3) (0) | 2023.04.07 |
Spring Boot 복습 (1) (0) | 2023.04.04 |
웹 개발 공부 (MyBatis) (0) | 2022.11.16 |