#4 : 엔티티
01.엔티티의 속성 구상하기
- 질문 엔티티에 필요한 최소한의 속성 : id, subject, content, create_date
- 답변 엔티티에 필요한 최소한의 속성 : id, question, content, create_date
02. 질문 엔티티 작성하기
- Question 클래스
package com.mysite.sbb.Entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
@Getter
@Setter
@Entity
public class Question {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(length = 200)
private String subject;
@Column(columnDefinition = "TEXT")
private String content;
private LocalDateTime createDate;
}
* @Entity : JPA가 해당 클래스를 엔티티로 인식할 수 있도록 사용
* @Getter, @Setter : 롬복의 어노테이션, Getter, Setter 메서드 자동 생성
* @Id : 해당 속성을 기본키(primary key)로 지정
* @GeneratedValue : 데이터 저장 시 해당 속성에 값이 1씩 증가하여 자동 저장,
strategy는 고유 번호를 생성하는 옵션으로 GenerationType.IDENTITY 지정 시 해당 컬럼만의 독립적인 시퀀스를 생성,
strategy 생략 시 @GeneratedValue 가 지정된 컬럼 모두 동일한 시퀀스로 번호 생성
* @Column : 컬럼의 세부설정을 위한 어노테이션, length는 컬럼의 길이를 설정할 때 사용, columnDefinition은 컬럼의 속성을 정의할 때 사용 (@Transient : 테이블 컬럼으로 인식하고 싶지 않은 경우에만 사용)
* Carmel Case : createDate 속성은 카멜케이스의 이름이기 때문에 실제 테이블의 컬럼명은 create_date가 된다.
* Setter : 일반적으로 엔티티에는 Setter메서드를 구현하지 않기를 권한다. 데이터베이스와 바로 연결되어 있으므로 안전하지 않다고 판단하기 때문이다. 엔티티 생성 시 롬복의 @Builder 어노테이션을 통한 빌드패턴을 사용하고 데이터 변경 시에는 그에 해당되는 메서드를 엔티티에 추가하여 변경한다. (본 강의에서는 우선 Setter 사용)
02. 답변 엔티티 작성하기
- Answer 클래스
package com.mysite.sbb.Entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
@Getter
@Setter
@Entity
public class Answer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(columnDefinition = "TEXT")
private String content;
private LocalDateTime createDate;
@ManyToOne
private Question question;
}
* 질문 엔티티를 참조하기 위해 question 속성 추가, answer.getQuestion().getSubject()의 형태로 접근 가능
* @ManyToOne : 이 때 질문 엔티티와 연결된 속성이라는 것을 명시하기 위해 해당 어노테이션 추가, 부모 자식 관계를 갖는 구조에 사용 가능하다. ( Question에서 Answer을 참조 시에는 @OneToMany 사용, 답변과 질문은 N:1 관계 )
이를 토대로 아래처럼 Question을 수정 가능하다.
package com.mysite.sbb.Entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.List;
@Getter
@Setter
@Entity
public class Question {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(length = 200)
private String subject;
@Column(columnDefinition = "TEXT")
private String content;
private LocalDateTime createDate;
@OneToMany(mappedBy = "question", cascade = CascadeType.REMOVE)
private List<Answer> answerList;
}
* 이제 질문 객체에서 답변을 참조하려면 question,getAnswerList()를 호출하면 된다.
* mappedBy : 참조 엔티티의 속성명을 의미한다. Answer 엔티티에서 참조한 속성명 question을 전달한다.
* cascade : 질문이 삭제되면 여러개의 답변들도 모두 삭제되도록 CascadeType.REMOVE를 설정
02. 테이블 확인하기
- H2 콘솔 접속 (http://localhost:8080/h2-console)

엔티티에 작성한 대로 테이블이 생성된 걸 확인 가능
#5 : 리포지토리
01.리포지토리
- 데이터 처리를 위해 실제 데이터베이스와 연동하는 JPA 리포지토리 필요
- 리포지토리 : 엔티티에 의해 생성된 데이터베이스 테이블에 접근하는 메서드들을 사용하기 위한 인터페이스
- QuestionRepository 생성
package com.mysite.sbb.repository;
import com.mysite.sbb.entity.Question;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Question, Integer> {
}
* 리포지토리는 JpaRepository를 상속받고, 제네릭스 타입에 <대상 엔티티의 타입, 대상 엔티티 기본키의 타입>을 작성해준다.
- AnswerRepository 생성
package com.mysite.sbb.repository;
import com.mysite.sbb.entity.Answer;
import org.springframework.data.jpa.repository.JpaRepository;
public interface AnswerRepository extends JpaRepository<Answer, Integer> {
}
02. 데이터 저장하기
- 테스트를 위해 테스트 프레임워크 ApplicationTests.java 파일 수정
package com.mysite.sbb;
import com.mysite.sbb.entity.Question;
import com.mysite.sbb.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 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 : 해당 클래스가 스프링부트 테스트 클래스임을 명시
* @Autowierd : 스프링의 DI(Dependency Injection) 기능, 객체를 스프링이 자동으로 생성, 순환참조와 같은 문제때문에
* @Autowierd보다는 생성자를 통한 객체 주입 방식이 권장되지만 테스트 코드의 경우 생성자를 통한 객체 주입이 불가능하므로 이를 사용한다.
* @Test : 해당 메서드가 테스트 메서드임을 명시
* JUnit Test로 실행하면 테스트 클래스를 실행 가능하다. 테스트를 위해서는 로컬서버를 중지한 후에 실행해야한다, 그 후 h2 콘솔에 아래 명령어를 실행 후 생성된 컬럼을 확인한다.
SELECT * FROM QUESTION
03. 데이터 조회하기
- 테스트코드를 다음처럼 수정
package com.mysite.sbb;
import com.mysite.sbb.entity.Question;
import com.mysite.sbb.repository.QuestionRepository;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.time.LocalDateTime;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
@RunWith(SpringRunner.class)
@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());
}
}
* findall() : 저장된 모든 데이터를 조회할 때 사용하는 메서드
* assertEquals() : 기대값과 실제값이 동일할지를 조사한다. assertEquals(기대값, 실제값)의 형태로 사용, 일치하면 테스트는 성공, 불일치하면 실패로 처리된다.
04. 아이디로 데이터 조회하기
- 테스트 코드 수정
package com.mysite.sbb;
import com.mysite.sbb.entity.Question;
import com.mysite.sbb.repository.QuestionRepository;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
@RunWith(SpringRunner.class)
@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());
}
}
}
* findById() : 아이디로 값을 조회하는 메서드, 이 메서드의 리턴타입은 Question이 아닌 Optional이다. null 처리를 유연하게 하기 위해 사용하는 클래스로, 위와 같이 isPresent로 null이 아닌지를 확인 후 get으로 실제 Question 객체 값을 얻는다.
05. 속성으로 데이터 조회하기
- 인터페이스 메서드 추가하기
package com.mysite.sbb.repository;
import com.mysite.sbb.entity.Question;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Question, Integer> {
Question findBySubject(String subject);
}
* 기본적으로 제공하지 않는 메서드는 리포지토리에 직접 선언해주면 JPA가 해당 메서드명을 분석하여 쿼리를 만들고 실행한다.
- 테스트 코드 수정하기
package com.mysite.sbb;
import com.mysite.sbb.entity.Question;
import com.mysite.sbb.repository.QuestionRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import static org.junit.jupiter.api.Assertions.assertEquals;
@RunWith(SpringRunner.class)
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
public void testJpa() {
Question q = this.questionRepository.findBySubject("sbb가 무엇인가요?");
assertEquals(1,q.getId());
}
}
* 실행되는 쿼리 콘솔에서 보기 - application.properties 파일 수정
# 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
그러면 where 조건에 subject 속성이 포함된 것을 확인 가능
06. 두 가지 속성으로 데이터 조회
- 인터페이스에 메서드 추가
package com.mysite.sbb.repository;
import com.mysite.sbb.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);
}
- 테스트 코드 수정하기
package com.mysite.sbb;
import com.mysite.sbb.entity.Question;
import com.mysite.sbb.repository.QuestionRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import static org.junit.jupiter.api.Assertions.assertEquals;
@RunWith(SpringRunner.class)
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
public void testJpa() {
Question q = this.questionRepository.findBySubjectAndContent("sbb가 무엇인가요?","sbb에 대해서 알고 싶습니다.");
assertEquals(1,q.getId());
}
}
* And외에도 Or, Between, LessThan, GreaterThanEqual, Like, In, OrderBy 등을 사용 가능하다.
07. Like로 포함된 데이터 조회하기
- 인터페이스에 메서드 추가
package com.mysite.sbb.repository;
import com.mysite.sbb.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> findBySubjectLike(String subject);
}
- 테스트 코드 수정
package com.mysite.sbb;
import com.mysite.sbb.entity.Question;
import com.mysite.sbb.repository.QuestionRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
@RunWith(SpringRunner.class)
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
public void testJpa() {
List<Question> qList = this.questionRepository.findBySubjectLike("sbb%");
Question q = qList.get(0);
assertEquals("sbb가 무엇인가요?",q.getSubject());
}
}
08. 데이터 수정하기
- 데스트 코드 수정
package com.mysite.sbb;
import com.mysite.sbb.entity.Question;
import com.mysite.sbb.repository.QuestionRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertTrue;
@RunWith(SpringRunner.class)
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
public void testJpa() {
Optional<Question> oq = this.questionRepository.findById(1);
assertTrue(oq.isPresent());
Question q = oq.get();
q.setSubject("수정된 제목");
this.questionRepository.save(q);
}
}
* assertTrue는 값이 true인지 테스트한다.
* 질문데이터를 조회한 다음 subjecy를 수정한 후 저장, 콘솔로그엔 update가 표시된다.
09. 데이터 삭제하기
- 테스트 코드 수정
package com.mysite.sbb;
import com.mysite.sbb.entity.Question;
import com.mysite.sbb.repository.QuestionRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@RunWith(SpringRunner.class)
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
public 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로 총 데이터 수를 확인한다.
10. 답변 데이터 생성 후 저장
- 테스트 코드 수정
package com.mysite.sbb;
import com.mysite.sbb.entity.Answer;
import com.mysite.sbb.entity.Question;
import com.mysite.sbb.repository.AnswerRepository;
import com.mysite.sbb.repository.QuestionRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@RunWith(SpringRunner.class)
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Autowired
private AnswerRepository answerRepository;
@Test
public 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); //어떤 질문의 답변인지 알기 위해
a.setCreateDate(LocalDateTime.now());
this.answerRepository.save(a);
}
}
* 답변 리포지토리도 @Autiwierd를 통해 주입
* 아이디가 2인 질문데이터를 가져와서 Answer의 question 속성에 대입하여 답변 데이터 생성, 저장
11. 답변에 연결된 질문찾기 vs 질문에 달린 답변 찾기
- a.getQuestion()을 이용하면 답변에 연결된 질문을 조회할 수 있다, 반대의 경우 또한 리스트를 이용하면 조회 가능하다.
- 테스트 코드 수정
package com.mysite.sbb;
import com.mysite.sbb.entity.Answer;
import com.mysite.sbb.entity.Question;
import com.mysite.sbb.repository.AnswerRepository;
import com.mysite.sbb.repository.QuestionRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@RunWith(SpringRunner.class)
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Autowired
private AnswerRepository answerRepository;
@Test
public void testJpa() {
Optional<Question> oq = this.questionRepository.findById(2);
assertTrue(oq.isPresent());
Question q = oq.get();
List<Answer> aList = q.getAnswerList();
assertEquals(1, aList.size());
assertEquals("네, 자동으로 생성됩니다.", aList.get(0).getContent());
}
}
- 하지만 위의 코드를 실행하면 아래와 같은 오류가 발생한다.
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.mysite.sbb.Question.answerList, could not initialize proxy - no Session
(... 생략 ...)
* findById로 객체를 조회하고 나면 DB세션이 끊어지기 때문인데, 테스트 코드에서만 발생하는 오류이다.
* 이렇게 필요한 시점에만 데이터를 가져오는 방식을 Lazy 방식이라고 한다. 이와 반대로 객체를 조회할 때 답변리스트를 모두 가져오는 방식을 Eager 방식이라고 한다. @OneToMany 등의 어노테이션의 fetch 옵션으로 지정할 수 있다.
* 테스트 코드 자체에서 이를 방지하여면 @Transactional 어노테이션을 사용한다.
package com.mysite.sbb;
import com.mysite.sbb.entity.Answer;
import com.mysite.sbb.entity.Question;
import com.mysite.sbb.repository.AnswerRepository;
import com.mysite.sbb.repository.QuestionRepository;
import jakarta.transaction.Transactional;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@RunWith(SpringRunner.class)
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Autowired
private AnswerRepository answerRepository;
@Transactional
@Test
public void testJpa() {
Optional<Question> oq = this.questionRepository.findById(2);
assertTrue(oq.isPresent());
Question q = oq.get();
List<Answer> aList = q.getAnswerList();
assertEquals(1, aList.size());
assertEquals("네, 자동으로 생성됩니다.", aList.get(0).getContent());
}
}
'T-I-L > [책] 요약&정리' 카테고리의 다른 글
| [점프 투 스프링부트] 2장 스프링부트의 기본 요소(답변등록,부트스트랩) - 2023. 08. 18 (0) | 2023.08.18 |
|---|---|
| [점프 투 스프링부트] 2장 스프링부트의 기본 요소(질문상세) - 2023. 08. 17 (0) | 2023.08.17 |
| [점프 투 스프링부트] 2장 스프링부트의 기본 요소(질문목록, 템플릿, 서비스) - 2023. 08. 16 (0) | 2023.08.16 |
| [점프 투 스프링부트] 2장 스프링부트의 기본 요소(프로젝트구성, 컨트롤러, JPA) - 2023. 08. 11 (0) | 2023.08.12 |
| [점프 투 스프링부트] 1장 스프링부트 개발 준비 - 2023. 08. 10 (0) | 2023.08.10 |
