기록방
14장 : 댓글 엔티티와 리포지토리 만들기 본문
길벗 IT도서에서 주관하는 코딩 자율학습단 8기 : Spring Boot 파트에 참여한 기록입니다 [ 목록 ]
14.1 댓글 기능의 개요
14.1.1 댓글과 게시글의 관계
- 게시글 입장
- 하나의 게시글에 여러 댓글이 달림
- one-to-many
- 일대다(1:n) 관계
- 댓글 입장
- 여러 댓글이 하나의 게시글에 달림
- many-to-one
- 다대일(n:1) 관계
- comment 테이블의 외래키로 article 테이블의 대표키를 갖고 있음
- 대표키(PK, Primary Key)
- id와 같이 자신을 대표하는 속성
- 테이블 내에서 중복된 값이 없어야 함
- 외래키(FK, Foreign Key)
- comment 테이블의 article_id처럼 연관 대상을 가리키는 속성
- 대표키(PK, Primary Key)
14.1.2 댓글 엔티티와 리포지토리 설계
- JPA(Java Persistence API) : 자바로 DB에 명령을 내리게 하는 도구로, 데이터를 객체 지향적으로 다루는 기술
- 엔티티(Entity) : DB 데이터를 담는 자바 객체로, 엔티티를 기반으로 테이블 생성
- 댓글 작성을 위한 엔티티 Comment 생성
- 리포지토리(Repository) : 엔티티를 관리하는 인터페이스로, 데이터 CRUD 등의 기능 제공
- CommentRepository 생성
- 기존의
ArticleRepository
처럼CrudRepository
를 상속하지 않고,JpaRepository
상속 - JpaRepository
ListCrudRepository
와ListPagingAndSortingRepository
를 상속받은 인터페이스- CURD(생성, 조회, 수정, 삭제) 기능뿐만 아니라 엔티티를 페이지 단위로 조회 및 정렬하는 기능과 PJA에 특화된 여러 기능 등을 제공
JpaRepository 인터페이스의 계층 구조
- Repository ← CrudRepository ← ListCrudRepository ← JpaRepository
- Repository ← PagingAndSortingRepository ← ListPagingAndSortingRepository ← JpaRepository
- 각 인터페이스의 기능
Repository
: 최상위 리포지토리 인터페이스CurdRepository
및ListCrudRepository
: 엔티티의 CRUD 기능 제공PagingAndSortingRepository
및ListPagingAndSortingRepository
: 엔티티의 페이징 및 정렬 기능 제공JpaRepository
: 엔티티의 CRUD 기능과 페이징 및 정렬 기능 뿐만 아니라 JPA에 특화된 기능을 추가로 제공
14.2 댓글 엔티티 만들기
14.2.1 댓글 엔티티 만들기
package com.example.firstproject.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Entity
@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // DB가 자동으로 1씩 증가
private Long id; // 대표키
@ManyToOne // Comment 엔티티와 Article 엔티티를 다대일 관계로 설정
@JoinColumn(name="article_id") // 외래키 설정, Article 엔티티의 기본키(id)와 맵핑
private Article article; // 해당 댓글의 부모 게시글
@Column // 해당 필드를 테이블의 속성으로 매핑
private String nickname; // 댓글을 단 사람
@Column // 해당 필드를 테이블의 속성으로 매핑
private String body; // 댓글 본문
}
- entity 패키지에
Comment.java
생성
- 실행하면 테이블을 만드는 걸 확인할 수 있음
- DB에 테이블이 잘 만들어진 것을 확인할 수 있음
14.2.2 더미 데이터 추가하기
INSERT INTO article(title, content) VALUES ('가가가가', '1111');
INSERT INTO article(title, content) VALUES ('나나나나', '2222');
INSERT INTO article(title, content) VALUES ('다다다다', '3333');
INSERT INTO member(id, email, password) VALUES (1, 'mail@cloudstudying.kr', '1111');
INSERT INTO member(id, email, password) VALUES (2, 'sehongpark@cloudstudying.kr', '2222');
INSERT INTO member(id, email, password) VALUES (3, 'email@cloudstudying.kr', '3333');
-- 11장 셀프체크 데이터 추가
INSERT INTO coffee(name, price) VALUES ('아메리카노', '4500');
INSERT INTO coffee(name, price) VALUES ('라떼', '5000');
INSERT INTO coffee(name, price) VALUES ('카페 모카', '5500');
-- 14장 데이터 추가
INSERT INTO article(title, content) VALUES ('당신의 인생 영화는?', '댓글 고');
INSERT INTO article(title, content) VALUES ('당신의 소울 푸드는?', '댓글 고고');
INSERT INTO article(title, content) VALUES ('당신의 취미는?', '댓글 고고고');
-- 4번 게시글의 댓글 추가
INSERT INTO comment(article_id, nickname, body) VALUES (4, 'Park', '굿 윌 헌팅');
INSERT INTO comment(article_id, nickname, body) VALUES (4, 'Kim', '아이 엠 샘');
INSERT INTO comment(article_id, nickname, body) VALUES (4, 'Choi', '쇼생크 탈출');
-- 5번 게시글의 댓글 추가
INSERT INTO comment(article_id, nickname, body) VALUES (5, 'Park', '치킨');
INSERT INTO comment(article_id, nickname, body) VALUES (5, 'Kim', '샤브샤브');
INSERT INTO comment(article_id, nickname, body) VALUES (5, 'Choi', '초밥');
-- 6번 게시글의 댓글 추가
INSERT INTO comment(article_id, nickname, body) VALUES (6, 'Park', '조깅');
INSERT INTO comment(article_id, nickname, body) VALUES (6, 'Kim', '유튜브 시청');
INSERT INTO comment(article_id, nickname, body) VALUES (6, 'Choi', '독서');
14.2.3 댓글 조회 쿼리 연습하기
특정 게시글의 모든 댓글 조회
SELECT * FROM comment WHERE article_id = 4;
특정 닉네임의 모든 댓글 조회
SELECT * FROM comment WHERE nickname = 'Park';
🚀 1분 퀴즈
- 댓글(comment)과 게시글(article) 테이블은 ( 다대일 ) 관계다.
- JPA에서 다대일 관계는 ( @ManyToOne ) 애노테이션으로 정의한다.
- 데이터의 관계는 대표키와 ( 외래키 )의 연결로 이뤄진다.
- 댓글(comment) 테이블에서 외래키는 ( article_id )(이)다.
- JPA에서 외래키 지정은 ( @JoinColumn ) 애노테이션으로 한다.
14.3 댓글 리포지토리 만들기
Comment 엔티티를 DB로 저장할 수 있게 댓글 리포지토리를 만들고 테스트 코드로 검증하기
14.3.1 댓글 리포지토리 만들기
- 네이티브 쿼리 메서드(native query method)
- 직접 작성한 SQL 쿼리를 리포지토리 메서드로 실행할 수 있게 함
- @Query 애노테이션을 사용하거나 orm.xml 파일 이용
특정 게시글의 모든 댓글 조회
- @Query 애노테이션 형식
- @Query 애노테이션은 SQL과 유사한 JPQL(Java Persistence Query Language)이라는 객체 지향 쿼리 언어를 통해 복잡한 쿼리 처리를 지원
- 그러나 nativeQuery 속성을 true로 하면 SQL문을 그대로 사용 가능
- 주의할 점은 WHERE 절에 조건을 쓸 때 매개 변수 앞에는 꼭 콜론(:)을 붙여야 함
@Query(value = "쿼리", nativeQuery = true)
// 특정 게시글의 모든 댓글 조회
@Query(value = "SELECT * FROM comment WHERE article_id = :articleId",
nativeQuery = true) // value 속성에 실행하려는 쿼리 작성
List<Comment> findByArticleId(Long articleId);
특정 닉네임의 모든 댓글 조회
- 네이티브 쿼리 XML(native query XML)
- 리포지토리에서 수행할 SQL쿼리문을 XML로 작성
- 기본 경로와 파일 이름은
resources > META-INF > orm.xml
- 형식
<named-native-query
name="쿼리_수행_대상_엔티티.메서드_이름"
result-class="쿼리_수행_결과_반환하는_타입의_전체_패키지_경로">
<query>
<![CDATA[
<!-- 쿼리 -->
]]>
</query>
</named-native-query>
- 최종 입력값
<?xml version="1.0" encoding="utf-8" ?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm
https://jakarta.ee/xml/ns/persistence/orm/orm_3_0.xsd"
version="3.0">
<named-native-query
name="Comment.findByNickname"
result-class="com.example.firstproject.entity.Comment">
<query>
<![CDATA[
SELECT * FROM comment WHERE nickname = :nickname
]]>
</query>
</named-native-query>>
</entity-mappings>
14.3.2 댓글 리포지토리 테스트 코드 작성하기
@DisplayName
애노테이션 사용- 메서드 명을 바꾸는 대신 테스트 이름을 변경
- 형식 :
@DisplayName(”테스트_결과에_보여_줄_이름”)
package com.example.firstproject.repository;
import com.example.firstproject.entity.Article;
import com.example.firstproject.entity.Comment;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@DataJpaTest
class CommentRepositoryTest {
@Autowired
CommentRepository commentRepository;
@Test
@DisplayName("특정 게시글의 모든 댓글 조회")
void findByArticleId() {
// Case 1 : 4번 게시글의 모든 댓글 조회
{
// 1. 입력 데이터 준비
Long articleId = 4L;
// 2. 실제 데이터
List<Comment> comments = commentRepository.findByArticleId(articleId);
// 3. 예상 데이터
Article article = new Article(4L, "당신의 인생 영화는?", "댓글 고");
Comment a = new Comment(1L, article, "Park", "굿 윌 헌팅");
Comment b = new Comment(2L, article, "Kim", "아이 엠 샘");
Comment c = new Comment(3L, article, "Choi", "쇼생크 탈출");
List<Comment> expected = Arrays.asList(a, b, c);
// 4. 비교 및 검증
assertEquals(expected.toString(), comments.toString(), "4번 글의 모든 댓글을 출력!");
}
// Case 2 : 1번 게시글의 모든 댓글 조회
{
// 1. 입력 데이터 준비
Long articleId = 1L;
// 2. 실제 데이터
List<Comment> comments = commentRepository.findByArticleId(articleId);
// 3. 예상 데이터
Article article = new Article(1L, "가가가가", "1111");
List<Comment> expected = Arrays.asList();
// 4. 비교 및 검증
assertEquals(expected.toString(), comments.toString(), "1번 글은 댓글이 없음");
}
}
@Test
@DisplayName("특정 닉네임의 모든 댓글 조회")
void findByNickname() {
// Case 1 : "Park"의 모든 댓글 조회
{
// 1. 입력 데이터 준비
String nickname = "Park";
// 2. 실제 데이터
List<Comment> comments = commentRepository.findByNickname(nickname);
// 3. 예상 데이터
Comment a = new Comment(1L, new Article(4L, "당신의 인생 영화는?", "댓글 고"),
nickname, "굿 윌 헌팅");
Comment b = new Comment(4L, new Article(5L, "당신의 소울 푸드는?", "댓글 고고"),
nickname, "치킨");
Comment c = new Comment(7L, new Article(6L, "당신의 취미는?", "댓글 고고고"),
nickname, "조깅");
List<Comment> expected = Arrays.asList(a, b, c);
// 4. 비교 및 검증
assertEquals(expected.toString(), comments.toString(), "Park의 모든 댓글을 출력!");
}
}
}
🚀 1분 퀴즈
- ( JpaRepository )은/는 ListCrudRepository와 ListPagingAndSortingRepository로부터 확장된 리포지토리 인터페이스다.
- ( @Query ) 애노테이션을 사용하면 리포지토리가 수행할 SQL 문을 직접 명시할 수 있다.
- 리포지토리가 수행할 SQL 문을 ( XML ) 파일로 만들어 연결할 수도 있다.
- 리포지토리와 엔티티 등의 테스트, 즉 JPA 영역의 테스트는 ( @DataJpaTest ) 애노테이션으로 명시한다.
✅ 셀프 체크
본문의 CommentRepositoryTest에 다음 6가지 테스트 케이스 코드를 추가하세요.
1. 9번 게시글의 모든 댓글 조회
// Case 3 : 9번 게시글의 모든 댓글 조회
{
// 1. 입력 데이터 준비
Long articleId = 9L;
// 2. 실제 데이터
List<Comment> comments = commentRepository.findByArticleId(articleId);
// 3. 예상 데이터
Article article = null;
List<Comment> expected = Arrays.asList();
// 4. 비교 및 검증
assertEquals(expected.toString(), comments.toString(), "9번 글은 없음");
}
2. 999번 게시글의 모든 댓글 조회
// Case 4 : 999번 게시글의 모든 댓글 조회
{
// 1. 입력 데이터 준비
Long articleId = 999L;
// 2. 실제 데이터
List<Comment> comments = commentRepository.findByArticleId(articleId);
// 3. 예상 데이터
Article article = null;
List<Comment> expected = Arrays.asList();
// 4. 비교 및 검증
assertEquals(expected.toString(), comments.toString(), "999번 글은 없음");
}
3. -1번 게시글의 모든 댓글 조회
// Case 5 : -1번 게시글의 모든 댓글 조회
{
// 1. 입력 데이터 준비
Long articleId = -1L;
// 2. 실제 데이터
List<Comment> comments = commentRepository.findByArticleId(articleId);
// 3. 예상 데이터
Article article = null;
List<Comment> expected = Arrays.asList();
// 4. 비교 및 검증
assertEquals(expected.toString(), comments.toString(), "-1번 글은 없음");
}
4. “Kim”의 모든 댓글 조회
// Case 2 : "Kim"의 모든 댓글 조회
{
// 1. 입력 데이터 준비
String nickname = "Kim";
// 2. 실제 데이터
List<Comment> comments = commentRepository.findByNickname(nickname);
// 3. 예상 데이터
Comment a = new Comment(2L, new Article(4L, "당신의 인생 영화는?", "댓글 고"),
nickname, "아이 엠 샘");
Comment b = new Comment(5L, new Article(5L, "당신의 소울 푸드는?", "댓글 고고"),
nickname, "샤브샤브");
Comment c = new Comment(8L, new Article(6L, "당신의 취미는?", "댓글 고고고"),
nickname, "유튜브 시청");
List<Comment> expected = Arrays.asList(a, b, c);
// 4. 비교 및 검증
assertEquals(expected.toString(), comments.toString(), "Kim의 모든 댓글을 출력!");
}
5. null의 모든 댓글 조회(특정 닉네임의 입력값이 null일 때)
// Case 3 : null의 모든 댓글 조회
{
// 1. 입력 데이터 준비
String nickname = null;
// 2. 실제 데이터
List<Comment> comments = commentRepository.findByNickname(nickname);
// 3. 예상 데이터
List<Comment> expected = Arrays.asList();
// 4. 비교 및 검증
assertEquals(expected.toString(), comments.toString(), "null의 모든 댓글을 출력!");
}
6. “”의 모든 댓글 조회(특정 닉네임의 입력값이 없을 때)
// Case 4 : ""의 모든 댓글 조회
{
// 1. 입력 데이터 준비
String nickname = "";
// 2. 실제 데이터
List<Comment> comments = commentRepository.findByNickname(nickname);
// 3. 예상 데이터
List<Comment> expected = Arrays.asList();
// 4. 비교 및 검증
assertEquals(expected.toString(), comments.toString(), "의 모든 댓글을 출력!");
}
🏓 더 알아볼 내용
0. 들어가기에 앞서
아직 ‘다대일’, ‘일대다’라는 용어가 익숙하지 않을 수 있습니다. 이는 DB에서 테이블과 테이블 간의 연관 관계를 나타내는 말로, 이 외에도 일대일 관계, 다대다 관계 등의 관계가 존재합니다. DB의 관계에 대한 이해가 부족한 분들은 본 학습 가이드에서 설명하는 것이 충분하지 않을 수 있으니 해당 개념을 보충 학습할 것을 추천합니다.
이번 학습 가이드에서는 ‘학생’과 ‘반’을 예로 들어 설명하겠습니다.
1. @ManyToOne
@ManyToOne은 N:1(다대일) 관계입니다. 예를 들어 반에는 여러 명의 학생이 있을 수 있습니다. 때문에 학생:반 = N:1 관계입니다. 이때 학생은 아이디, 이름, 반 정보를 가지고 있습니다. 이를 코드로 작성하면 다음과 같습니다.
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Entity
public class Student {
@Id
@GeneratedValue
private Long id;
private String name;
private Clazz clazz;
}
Student 클래스에 id(아이디), name(이름), clazz(반) 필드를 선언했습니다. 이어서 Clazz 클래스 코드를 보겠습니다.
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Entity
public class Clazz {
@Id
@GeneratedValue
private Long id;
private int grade;
}
위의 Clazz 클래스 코드를 보면 학생 정보가 없습니다. 그렇다면 어떻게 학생과 반의 관계를 N:1로 설정하는 걸까요? 바로 @ManyToOne 어노테이션을 사용합니다. 다음과 같이 말이죠.
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Entity
public class Student {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne
private Clazz clazz;
}
위 코드를 보면 Student 클래스의 Clazz 필드 위에 @ManyToOne 애노테이션을 붙였습니다.
여기서 Many는 Student를 가리키고 One은 Clazz를 가리킵니다. 학생과 반은 N:1 관계이기 때문에 Student 클래스에 @ManyToOne 애노테이션을 붙이는 것입니다. 이렇게 하면
Student 테이블과 Clazz 테이블이 N:1 관계를 맺게 됩니다.
2. @OneToMany
@OneToMany는 1:N(일대다) 관계입니다. 앞서 학생:반 = N:1 관계였는데요. 이를 뒤집으면반:학생 = 1:N 관계입니다. 한 반에 여러 학생이 있다는 말입니다. 다음과 Clazz 클래스에
@OneToMany 애노테이션을 사용하면 그 반에 어떤 학생들이 있는지 알 수 있습니다.
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Entity
public class Clazz {
@Id
@GeneratedValue
private Long id;
private int grade;
@OneToMany(mappedBy = "clazz") // The field that owns the relationship.
Set<Student> students;
}
@ManyToOne과 @OneToMany 애노테이션을 활용해 코드를 작성하면 다음과 같습니다.
이렇게 연관 관계를 이용하면, 학생을 생성했을 때 Clazz에도 학생 정보가 들어가는 것을 볼 수 있습니다.
@SpringBootTest
class StudentServiceTest {
@Autowired
private StudentService studentService;
@Autowired
private StudentRepository studentRepository;
@Autowired
private ClazzService clazzService;
@Autowired
private ClazzRepository clazzRepository;
@Test
void createStudent() {
Clazz clazz = new Clazz(1, new ArrayList<>());
clazzRepository.save(clazz);
StudentCreateRequest studentCreateRequest = new StudentCreateRequest("test student", clazz.getId());
Student student = studentService.createStudent(studentCreateRequest);
assertThat(student.getId()).isNotNull();
assertThat(student.getName()).isEqualTo("test student");
assertThat(student.getClazz().getId()).isEqualTo(clazz.getId());
assertThat(student.getClazz().getStudents().size()).isEqualTo(1);
}
}
@Service
@RequiredArgsConstructor
public class StudentService {
private final StudentRepository studentRepository;
private final ClazzService clazzService;
@Transactional
public Student createStudent(StudentCreateRequest studentCreateRequest) {
Clazz clazz = clazzService.getClazz(studentCreateRequest.getClazzId());
Student student = new Student(studentCreateRequest.getName(), clazz);
clazz.getStudents().add(student);
return studentRepository.save(student);
}
}
'FrameWork > Spring' 카테고리의 다른 글
16장 : 웹 페이지에서 댓글 목록 보기 (0) | 2024.07.29 |
---|---|
15장 : 댓글 컨트롤러와 서비스 만들기 (0) | 2024.07.10 |
13장 : 테스트 코드 작성하기 (0) | 2024.07.09 |
12장 : 서비스 계층과 트랜잭션 (0) | 2024.07.09 |
11장 : HTTP와 REST 컨트롤러 (0) | 2024.07.06 |