포스트

스프링 부트3 제 14장

게시판을 만들었으니 댓글 한번 달아 볼까?

스프링 부트3 제 14장

댓글 엔티티와 리파지터리 설계

  • 엔티티: DB 데이터를 담는 자바 객체로, 엔티티를 기반으로 테이블 생성
  • 리파지터리: 엔티티를 관리하는 인터페이스로, 데이터 CRUD 등의 기능 제공

엔티티는 Article 엔티티를 만들듯이 만들면 된다. 단, 게시글과 일대다 관계를 유지하기 위해 @ManyToOne 어노테이션을 칼럼에 추가해야 한다.

3장에서 ArticleRepository를 만들 땐, CrudRepository를 사용했다. 이번엔 JpaRepository를 사용하여 CRUD뿐만 아니라 엔티티를 페이지 단위로 조회 및 정렬하는 기능과 JPA에 특화된 여러 기능들을 살펴보겠다.

댓글 엔티티

Comment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Entity // 해당 클래스가 엔티티임을 선언, 클래스 필드를 바탕으로 DB에 테이블 생성
@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Comment {
    @Id // id를 대표키로 지정
    @GeneratedValue(strategy= GenerationType.IDENTITY) // DB가 자동으로 매김
    private Long id;
    @ManyToOne // Comment와 Article의 관계를 일대다로 설정
    @JoinColumn(name="article_id") // 외래키 생성, Article의 기본키와 매핑
    private Article article;
    @Column // 해당 필드를 테이블의 속성으로 매핑
    private String nickname;
    @Column
    private String body;
}

@ManyToOne 어노테이션을 통해 Comment 테이블과 Article 테이블을 일대다 관계로 설정했다.
또한 @JoinColumn을 통해 외래 키를 생성하고, Article 테이블의 기본 키와 매핑했다.

댓글 리파지터리

CommentRepository

1
2
3
4
5
6
7
public interface CommentRepository extends JpaRepository<Comment, Long> {
    // 네이티브 쿼리 메서드를 사용하여 쿼리를 메서드로 작성할 것임
    @Query(value = "select * from comment where article_id = :articleId", nativeQuery = true) // value 속성에 실행하려는 쿼리 작성
    List<Comment> findByArticleId(Long articleId); // 특정 게시글의 모든 댓글 조회 메서드(@Query 어노테이션 사용)
    
    List<Comment> findByNickname(String nickname); // 특정 닉네임의 모든 댓글 조회 메서드(orm.xml 사용, resources > META-INF)
}

쿼리를 메서드에서 사용하는 네이티브 쿼리 메서드(native query method)를 사용했다. 이것을 만들기 위해선 @Query 어노테이션을 쓰거나, 네이티브 쿼리 XML(native query XML) 파일을 쓰는 두 가지 방법이 있다.

참고로 @Query 어노테이션은 SQL과 유사한 JPQL(Java Persistence Query Language)를 사용한다. 하지만 여기선 nativeQuerytrue로 설정하여 SQL을 그대로 사용했다.
그리고 SQL 문의 매개변수 앞에는 반드시 콜론(:)을 붙여줘야 한다.

orm.xml의 경로는 resources/META-INF/orm.xml 이어야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8" ?>
<entity-mappings xmlns="https://jakarta.ee/xml/ns/persistence/orm"
                 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                 xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence/orm
                 https://jakarta.ee/xml/ns/persistance/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>

테스트 코드

CommentRepositoryTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// ... 생략

@DataJpaTest // 해당 클래스를 JPA와 연동해 테스팅
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, "aaaa", "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, "당신의 인생 영화는?", "ㅈㄱㄴ"), "Park", "굿 윌 헌팅");
            Comment b = new Comment(4L, new Article(5L, "당신의 소울 푸드는?", "ㅈㄱㄴ2222"), "Park", "치킨");
            Comment c = new Comment(7L, new Article(6L, "당신의 취미는?", "ㅈㄱㄴ3333"), "Park", "조깅");
            List<Comment> expected = Arrays.asList(a, b, c);
            // 4. 비교 및 검증
            assertEquals(expected.toString(), comments.toString(), "Park의 모든 댓글 출력");
        }
    }
}

@SpringBootTest 대신 @DataJpaTest 어노테이션을 써서 JPA와 연동해 테스트했다.
그리고 @DisplayName을 사용해 테스트에 이름을 붙여주었다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.