본문 바로가기

SpringBoot

[Do it] 2장 스프링 부트 기본 기능 익히기(2)

1. 리포지터리로 데이터베이스 관리하기 

 

1) 리포지터리 생성

 

JPA 리포지터리 

 

- 데이터베이스 테이블의 데이터들을 저장, 조회, 수정, 삭제 등을 할 수 있도록 도와주는 인터페이스 

- 테이블에 접근하고, 데이터를 관리하는 메서드를 제공 

- CRUD 작업을 처리하는 메서드들을 이미 내장하고 있어 데이터 관리 작업을 좀 더 편리하게 처리할 수 있음

 

QuestionRepository.java

package com.example.demo;

import org.springframework.data.jpa.repository.JpaRepository;

public interface QuestionRepository extends JpaRepository<Question,Integer> {

}

 

- 생성한 QuestionRepository 인터페이스를 리포지터리로 만들기 위해 JpaRepository 인터페이스를 상속 

 

- JpaRepository<Question,Integer>

  -> Question 엔티티로 리포지터리를 생성

  -> Question 엔티티의 기본키가 Integer

 

 

2) JUnit 설치

 

JUnit

 

- 테스트 코드를 작성하고, 작성한 테스트 코드를 실행할 때 사용하는 자바의 테스트 프레임워크 

- 작성한 리포지터리가 정상적으로 동작하는지 테스트 

 

build.gradle

 

 

3) 질문 데이터 저장

 

-질문 엔티티로 만든 테이블에 데이터를 생성하고 저장

 

SbbApplicationTests.java

package com.example.demo;

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

}

 

@SpringBoodTest

 

- SbbApplicationTests 클래스가 스프링 부트의 테스트 클래스임을 의미 

 

@Autowired

 

- 질문 엔티티의 데이터를 생성할 때 리포지터리(QuestionRepository)가 필요

- @Autowired를 통해 스프링의 '의존성 주입(DI)'를 사용하여 QuestionRepository의 객체 주입 

- 스프링 부트가 questionRepository 객체를 자동으로 만들어 주입 

 

의존성 주입(DI, Dependency Injection)

 

- 스프링이 객체를 대신 생성하여 주입하는 기법 

- @Autowired 사용 / Setter 메서드  사용 / 생성자 사용

 

@Test

 

- testJpa 메서드가 테스트 메서드임을 나타냄 

- testJpa 메서드는 q1,q2라는 질문 엔티티의 객체를 생성하고 QustionRepository를 이용하여 그 값을 데이터베이스에 저장 

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

 

 

SbbApplicationTest 클래스 실행 

 

 

 

- H2 콘솔에 접속하여 Select * From Question 쿼리문 실행 

 

 

 

4) 질문 데이터 조회하기 

 

findAll 메서드 

 

- question 테이블에 저장된 모든 데이터를 조회하기 위해 사용 

 

SbbApplicationTests.java

package com.example.demo;

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());
		
        //첫 번째 데이터의 제목이 'sbb가 무엇인가요?' 데이터와 일치하는지 테스트
		Question q = all.get(0);
		assertEquals("sbb가 무엇인가요?", q.getSubject());
	}

}

 

assertEquals 메서드 

 

- assertEquals(기댓값, 실젯값)

- 기댓값과 실젯값이 동일하지 않다면 테스트는 실패로 처리 

- 2개의 질문 데이터를 저장했기 때문에 데이터 사이즈는 2가 되어야 함 

- Junit의 메소드로 테스트에서 예상한 결과와 실제 결과가 동일한지를 확인하는 목적으로 사용 

- JPA 또는 데이터베이스에서 데이터를 올바르게 가져오는지를 확인

 

 

findById 메서드 

 

- 질문 엔티티의 기본키인 id의 값을 활용해 데이터를 조회 

 

sbbApplication.java

package com.example.demo;

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() {
    	//데이터베이스에서 id가 1인 질문 조회 
		Optional<Question> oq = this.questionRepository.findById(1);
		if(oq.isPresent()) {
			Question q = oq.get();
			assertEquals("sbb가 무엇인가요?" ,q.getSubject());
		}
	}

}

 

- findById의 리턴 타입은 Question이 아닌 Optional

 -> findById로 호출한 값이 존재할 수도 있고, 존재하지 않을 수도 있어서 리턴 타입으로 Optional이 사용됨 

 

- isPresent()를 통해 값이 존재한다는 것을 확인했으면, get() 메서드를 통해서 실제 Question 객체의 값을 얻음 

 

Optional

- 값을 처리하기 위한 (null값을 유연하게 처리하기 위한)클래스

- isPresent() 메서드로 값이 존재하는지 확인 가능

 

 

findBySubject 메서드 

 

- 질문 엔티티의 subject 값으로 데이터를 조회 

- 기본적으로 제공하는 메서드가 아님

- QuestionRepository 인터페이스에 findBySubject 메서드 선언 

- JPA에 리포지터리의 메서드명을 분석하여 쿼리를 만들고 실행하는 기능이 있기 때문에  findBy + 엔티티의 속성명과 같은 리포지터리의 메서드를 작성하면 입력한 속성의 값으로 데이터를 조회 가능

 

QuestionRepository.java

package com.example.demo;

import org.springframework.data.jpa.repository.JpaRepository;

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

 

 

SbbApplicationTests.java

- subject 값으로 테이블에 저장된 데이터를 조회 

package com.example.demo;

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 메서드를 호출할 때 실제 데이터베이스에서는 어떤 쿼리문이 실행되는지 콘솔 로그에서 확인 

 

application.properties

 

 

findBySubjectAndContent 메서드 

 

- subject와 content를 함께 조회 

- SQL을 활용해 데이터베이스에서 두 개의 열(엔티티의 속성)을 조회하기 위해서는 And 연산자 사용 

 

QuestionRepository.java

- 리포지터리에 findBySubjectAndContent 메서드 추가 

package com.example.demo;

import org.springframework.data.jpa.repository.JpaRepository;

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

 

SbbApplicationTests.java

package com.example.demo;

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.findBySubejctAndContent("sbb가 무엇인가요?","sbb에 대해서 알고 싶습니다." );
		assertEquals(1,q.getId());
	}

}

 

- 리포지터리의 메서드명은 데이터를 조회하는 쿼리문의 where 조건을 결정하는 역할을 함 

 

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

 

 

findBySubjectLike 메서드 

 

- 질문 엔티티의 subject 열 값들 중에 특정 문자열을 포함하는 데이터를 조회 

 

QuestionRepository.java

- subject열에서 특정 문자열을 포함하는 데이터를 찾기 위해 findBySubjectLike 메서드를 리포지토리에 추가 

package com.example.demo;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;

public interface QuestionRepository extends JpaRepository<Question,Integer> {
	Question findBySubject(String subject);
	Question findBySubejctAndContent(String subject, String content);
	List<Question> findBySubjectLike(String subject);
}

 

SbbApplicationTests.java

package com.example.demo;

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

}

 

5) 질문 데이터 수정 

 

SbbApplicationTests.java

 

- 질문 엔티티의 데이터를 조회한 다음, subjec 속성을 '수정된 제목' 이라는 값으로 수정

- 변경된 질문을 데이터베이스에 저장하기 위해서 this.questionRepository.save(q)와 같이 리포지터리의 save 메서드 사용 

package com.example.demo;

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

}

 

 

 

6) 질문 데이터 삭제

 

- 첫 번째 질문 삭제

- 리포지터리의 delete 메서드를 사용하여 데이터를 삭제 

- 데이터 건수가 삭제하기 전에 2였는데, 삭제한 후 1이 되었는지를 테스트 

- 리포지터리의 count 메서드는 테이블 행의 개수를 리턴 

 

SbbApplicationTests.java

package com.example.demo;

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

}

 

 

 

7) 답변 데이터 저장 

 

- 답변 엔티티의 데이터를 생성하고 저장 

- 답변 데이터를 저장할 때도 리포지터리(AnswerRepository)가 필요하므로 AnswerRepository의 객체를 @Autowired를 통해 주입

- 답변을 생성하려면 질문이 필요하므로 우선 질문을 조회해야 함 

- questionRepository의 findById  메서드를 통해 id가 2인 질문 데이터를 가져와 답변의 question 속성에 대입해 답변 데이터를 생성 

 

SbbApplicationTests.java

package com.example.demo;

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() {
    	//question 열에 데이터를 생성하려면 질문 데이터를 조회해야 함
		Optional<Question>oq = this.questionRepository.findById(2);
		assertTrue(oq.isPresent());
		Question q = oq.get();
		
		Answer a = new Answer();
		a.setContent("네 자동으로 생성됩니다.");
        //답변 엔티티의 question 속성에 질문 데이터를 대입해 답변 데이터를 생성하려면
        //이와 같이 Question 객체 q가 필요 
		a.setQuestion(q);
		a.setCreateDate(LocalDateTime.now());
		this.answerRepository.save(a);
	}

}

 

 

 

8) 답변 데이터 조회 

 

- id값이 1인 답변을 조회

- 조회한 답변과 연결된 질문의 id가 2인지도 조회 

 

SbbApplicationTests.java

- 질문을 조회한 후 이 질문에 달린 답변 전체를 구하는 테스트 코드 

package com.example.demo;

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

}

 

 

9) 질문 데이터를 통해 답변 데이터 찾기 

 

- 질문 엔티티에 정의한 answerList를 사용

 

sbbApplicationTests.java

package com.example.demo;

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;
	
	@Autowired
	private AnswerRepository answerRepository;
	
	@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());
		
	}

}

 

 

오류

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

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

- answerList는 앞서 q 객체를 조회할 떄가 아니라 q.getAnswerList() 메서드를 호출하는 시점에 가져오기 때문

- 실제 서버에서 JPA 프로그램들을 실행할 때는 DB 세션이 종료되지 않아서 발생 x

- 테스트 코드에서만 발생 

 

지연(Lazy) 방식 - 데이터를 필요한 시점에 가져오는 방식
즉시(Eager) 방식 - q 객체를 조회할 때 미리 answer 리스트를 모두 가져오는 방식

 

 

해결

 

@Transactional 

 - 메서드가 종료될 때까지 DB 세션이 유지됨 

 

SbbApplicationTests.java

 

 

 

2. 도메인별로 분류하기 

 

- 자바 파일을 도메인에 따라 패키지로 나누어 관리 

- 비슷한 기능이나 관련된 개념을 함께 묶어 구조화하여 정리하게 되므로 코드를 쓰거나 읽을 때 유지 보수를 할 때 편리 

 

도메인 이름 패키지 이름 설명
question com.example.demo.question 게시판의 질문과 관련된 자바 파일
answer com.example.demo.answer 게시판의 답변과 관련된 자바 파일
user com.example.demo.user 사용자의 관련된 자바 파일