elevne's Study Note

Spring Boot 복습 (2) 본문

Backend/Spring

Spring Boot 복습 (2)

elevne 2023. 4. 5. 22:04

저번 시간에는 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 이라고 뜨는 것을 확인할 수 있다.

 

 

 

result

 

 

 

실제 DB 에도 잘 들어갔는지 console 에 들어가서 확인해보았다.

 

 

 

result

 

 

 

그 다음으로는 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 인지 확인하기 위해 JUnitassertEquals 메서드를 사용한다. 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

 

 

result

 

 

 

제목과 내용을 동시에 사용하여 조회해볼 수도 있다. 이 때는 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);

	}
}

 

 

result

 

 

 

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

	}
}

 

 

result

 

 

 

위 테스트 코드는 실패한다. 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());

	}
}

 

 

result

 

 

 

 

 

 

 

Reference:

https://wikidocs.net/161165

'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