본문 바로가기

⭐ SpringBoot/𝄜 게시판 with SpringBoot

21. 댓글 CRUD를 위한 Entity와 Repository를 생성

728x90
반응형

활용

1. PK와 FK를 활용하여 DB 테이블 조인관계를 설정
2. 새로운 테이블을 생성
3. 더미 데이터를 생성하여 새롭게 생성된 테이블에 저장
4. 저장된 데이터의 예상값과 실제값을 TDD를통해 테스트


# Comment(댓글) 엔티티와 레파지토리를 만들고, 이를 테스트 한다.

요약

댓글 기능을 만들기 위해 comment 엔티티 파일을 생성 하였다.

이 comment 엔티티는 Article 엔티티와 Many to One의 관계를 가지고 있었고, 즉 다대일 관계를 형성 하였고, JoinColumn을 통해 FK를 설정할 수 있었다. 여기서 FK는 외래키, PK는 대표키 이다.

또한 레파지토리에 SQL을 직접 작성하는 방법, 즉 Native Query Method또한 배웠는데 하나는 @Query 어노테이션을 통해서 작성을 하였고, 또 하나는 XML을 통해 작성을 진행 하였다. 마지막으로 작성된 Repository를 테스트 하였는데, 입력 데이터를 준비하고 실제 수행값을 체크, 예상값을 선정, 검증하는 과정까지 진행 하였다.

게시글과 댓글의 관계

하나의 게시글의 수많은 댓들이 생성된다. 이러한 관계를 One-to-Many 일대다 관계라고 한다.

거꾸로 댓글의 입장에서는 Many-to-One 다대일 관계라고 볼 수 있다.

이러한 데이터가 실제로 DB에 전송되는 과정

아래의 그림처럼 PK와 FK의 관계로 DB가 형성되어 있는것을 볼 수 있다.

댓글 엔티티 설계

다음과 같이 댓글에서 게시글을 찾아갈수 있도록 구성하면 된다.

JPA 레파지토리란?

데이터의 CRUD 뿐만 아니라 일정 페이지의 데이터 조회 및 정렬 기능도 제공 한다.

실제 코드를 통한 엔티티와 레파지토리를 구현

댓글 엔티티

이전에 생성한 엔티티 패키지 폴더에 Comment라는 새로운 클래스 파일을 생성한다.

Comment 파일의 내용

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) → DB가 자동증가
    private Long id;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

여러개의 댓글이 하나의 게시글에 입력 되는 구조이기 때문에 아래와 같이 설정한다.

    @ManyToOne // 해당 댓글 엔티티 여러개가, 하나의 Article에 연관된다.!
    @JoinColumn(name = "article_id")
    private Article article;

Comment Entity 파일의 전체 코드

@Entity
@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Comment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne // 해당 댓글 엔티티 여러개가, 하나의 Article에 연관된다.!
    @JoinColumn(name = "article_id") // 테이블에서 연결될 대상정보를 가져온다.
    private Article article;

    @Column
    private String nickname;

    @Column
    private String body;
}

실행 콘솔 화면을 inteliJ에서 확인해보면 아래의 코드가 실행되면서 하나의 테이블을 생성한다.

    create table comment (
       id bigint generated by default as identity,
        body varchar(255),
        nickname varchar(255),
        article_id bigint,
        primary key (id)
    )

엔티티를 통해서 생성된 commnet 테이블에 기본 더미 데이터를 추가해 보자.

더미 데이터는 data.sql에 넣는다.

resources → data.sql 파일에 더미 데이터를 생성하는 쿼리를 추가하여 더미 데이터를 생성해 보자.

data.sql의 파일 내용

INSERT INTO article(id, title, content) VALUES (1, '가가가가가', '11111');
INSERT INTO article(id, title, content) VALUES (2, '나나나나나', '22222');
INSERT INTO article(id, title, content) VALUES (3, '다다다다다', '33333');

-- article 더미 데이터 추가
INSERT INTO article(id, title, content) VALUES (4, '당신의 인생 영화는?', '댓글 ㄱ');
INSERT INTO article(id, title, content) VALUES (5, '당신의 소울 푸드는?', '댓글 ㄱㄱ');
INSERT INTO article(id, title, content) VALUES (6, '당신의 취미는?', '댓글 ㄱㄱㄱ');

-- comment 더미 데이터 추가
---- 4번 게시글의 댓글들
INSERT INTO comment(id, article_id, nickname, body) VALUES (1, 4, 'Park', '굳 윌 헌팅');
INSERT INTO comment(id, article_id, nickname, body) VALUES (2, 4, 'Kim', '아이 엠 샘');
INSERT INTO comment(id, article_id, nickname, body) VALUES (3, 4, 'Choi', '쇼생크의 탈출');

---- 5번 게시글의 댓글들
INSERT INTO comment(id, article_id, nickname, body) VALUES (4, 5, 'Park', '치킨');
INSERT INTO comment(id, article_id, nickname, body) VALUES (5, 5, 'Kim', '샤브샤브');
INSERT INTO comment(id, article_id, nickname, body) VALUES (6, 5, 'Choi', '초밥');

---- 6번 게시들의 댓글들
INSERT INTO comment(id, article_id, nickname, body) VALUES (7, 6, 'Park', '조깅');
INSERT INTO comment(id, article_id, nickname, body) VALUES (8, 6, 'Kim', '유투브');
INSERT INTO comment(id, article_id, nickname, body) VALUES (9, 6, 'Choi', '독서');

PK와 FK란?

댓글(Comment)과 게시글(Article)의 관계를 파악한다.

자기자신의 ID는 PK라고 하며, 상대받의 ID를 담는것을 FK 라고 한다.

그래서 One to Many 형태의 관계로 연결고리가 되어있다고 볼 수 있다.

Comment Repository를 생성

repository 패키지 폴더에 새로운 파일을 생성 후 CommentRepository 라는 파일 명으로 자바 인터페이스 파일을 생성한다. 

List<Comment> findByArticleId(Long articleId); → articleId를 입력 했을때, List<Commnent> 즉, Comment의 묶음을 반환 했으면 좋겠다. 그렇다면 findByArticleId가 원하는 쿼리를 실행 시켜줘야 한다.

쿼리 어노테이션을 통해서 데이터를 조회하는 것을 아래의 코드를 통해서 가능하다.

 @Query(value =
            "SELECT * " +
                    "FROM comment " +
                    "WHERE article_id = :articleId", // 요값과
            nativeQuery = true)
    List<Comment> findByArticleId(Long articleId); // 요값이 같아야 매칭되어 정상 조회 된다.

네이티브 쿼리 XML : 특정 닉네임의 모든 댓글을 조회하는 쿼리를 XML로 작성한다.

XML은 resources 경로에 새로운 디렉토리를 META-INF 라는 이름으로 생성한다. 

META-INF 경로에 orm.xml 이라는 파일을 생성해 준다.

orm.xml의 파일 내용은 아래와 같다.

name="Comment.findByNickname" → findByNickname 메소드는 아래의 SQL 쿼리문을 수행 할 것이다.

result-class="com.example.firstproject.entity.Comment" → 쿼리가 반환할 타입은 ...entity.Comment로 반환을 하겠다는 코드이다.

<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings
        xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm
            http://xmlns.jcp.org/xml/ns/persistence/orm_2_2.xsd"
        version="2.2">
    <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>

CommentRepository의 전체 코드

public interface CommentRepository extends JpaRepository<Comment, Long> {
    // 특정 게시글의 모든 댓글 조회
    @Query(value =
            "SELECT * " +
                    "FROM comment " +
                    "WHERE article_id = :articleId", // 요값과
            nativeQuery = true)
    List<Comment> findByArticleId(Long articleId); // 요값이 같아야 매칭되어 정상 조회 된다.

    // 특정 닉네임의 모든 댓글 조회
    List<Comment> findByNickname(String nickname);
}

레파지토리 테스트 A

테스트 생성 방법: 테스트를 원하는 메소드에서 Go To 클릭 → Test → Create New Test... →  테스트 할 메소드를 선택 후 → OK 를 클릭하여 테스트 기본 메소드를 생성한다.

실제 수행 : List<Comment> comments = commentRepository.findByArticleId(articleId); → commentRepository가 .findByArticleId를 통해서 어떤 대상을 리턴할 것이다. 파라미터로는 articleId가 입력될 것이고, comments를 리턴할 것이고 해당 리턴할 타입은 List<Comment>의 묶음일 것이다.

입력 데이터 준비 : (articleId)값이 없으므로 Long articleId = 4L; 즉 4번 게시글 이기 때문에 이와 같이 입력 한다.

검증 : assertEquals(expected.toString(), comments.toString() → 실제 값과 예상값을 비교

실제 수행 값에는 어떤 값이 있는지 DB를 통해서 확인해 본다. 4번 게시글의 모든 데이터이기 때문에 아래와 같이 3개의 데이터값이 출력될 것이다.

그래서 해당 데이터를 아래와 같이 예상값으로 만든다.

Article article = new Article(4L, "당신의 인생 영화는?", "댓글 ㄱ"); → 해당 데이터 테이블은 comment테이블이 아닌 Article 테이블이다.

리스트 형태로 바꿔줘야 하므로 이와 같이 변경 한다. → List<Comment> expected = Arrays.asList(a, b, c);

            // 예상하기
            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번 라인의 코멘트를 가져온게 우리가 예상했던 3개의 코멘트 값과 일치했다고 볼 수 있다.

해당 코드로 테스트를 수행하면 결과는 아래와 같다.

다음으로, Case 2: 1번 게시글의 모든 댓글 조회 하는 코드는 아래와 같다.

위의 Case 1과 진행 방식이 비슷하므로 참조하여 작성 후 테스트를 진행하면 된다. 특이점은 comment 테이블의 FK 값 즉 ARTICCLE_ID값이 1번이 존재하지 않으므로, 비어있는 값이 나올 것이다. 그래서 예상값과 실제값이 비어있는 채로 출력이 되어야 할 것이다.

닉네임을 가지고 댓글을 조회하는 테스트 (Park의 모든 댓글 조회 하는 작업을 테스트)

실제 수행 : commentRepository.findByNickname()을 가지고 댓글을 찾는데, nickname을 입력값으로 넣어준다. 그것을 반환한 값이 comments가 될것이고, comments의 타입은 Comment의 묶음인 List<Comment>가 될것이다.

입력 데이터 준비 : String 타입의 nickname을 넣어주는데 여기서 Park의 모든 데이터를 가져오기로 했으므로, 이와같이 입력해준다. String nickname = "Park";

예상하기 : 예상 데이터는 아래와 같을 것인데, 해당 데이터를 예상 값으로 추가해 주면 된다.

이전의 예제 코드 : Comment a = new Comment(1L, article, "Park", "굳 윌 헌팅"

여기서 특이한 점은 이전에는 위와 같이 article 데이터를 한번에 넣어 줬던것과 다르게,

아래에서는 new Article(4L, "당신의 인생 영화는?", "댓글 ㄱ")와 같이 해당 값을 다르게 넣어주는것을 확인 할 수 있다.

이것은 해당 값이 특정 테이블의 특정 라인값이 되어야 하기 때문에 이와같이 입력해 준다.

// 예상하기
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);

검증 : assertEquals(expected.toString(), comments.toString(), "Park의 모든 댓글을 출력!"); → 예상하는 값의 to.String() 값과 실제 값의 to.String()값이 같을것이다 라고 예상 한다. 추가적으로 식별 가능 하도록 메세지도 입력한다.

해당 메소드를 실제로 테스트 했을때 정상적으로 테스트가 완료된것을 아래와 같이 확인 할 수 있다.

그렇다면, 테스트 값을 다르게 하고 테스트를 실패 했을때 어떤 결과를 확인할 수 있는지 보자.

테스트를 위해 마지막의 특정 닉네임의 모든 데이터를 조회하는 테스트에서 a의 값을 1L → 2L으로 변경하고 테스트를 진행해 본다.

그렇다면 오류 내용이 아래와 같이 콘솔창에 예상값과 실제값이 다르다고 표시되는것을 확인 할 수 있다.

아래 코드를 보면, id값의 예상값이 2로, 실제 id값인 1 과 달라서 에러가 발생했다고 표시된것을 확인 할 수 있다.

org.opentest4j.AssertionFailedError: Park의 모든 댓글을 출력! ==> 
Expected :[Comment(id=2, article=Article(id=4, title=당신의 인생 영화는?, content=댓글 ㄱ), nickname=Park, body=굳 윌 헌팅), Comment(id=4, article=Article(id=5, title=당신의 소울 푸드는?, content=댓글 ㄱㄱ), nickname=Park, body=치킨), Comment(id=7, article=Article(id=6, title=당신의 취미는?, co ...
Actual   :[Comment(id=1, article=Article(id=4, title=당신의 인생 영화는?, content=댓글 ㄱ), nickname=Park, body=굳 윌 헌팅), Comment(id=4, article=Article(id=5, title=당신의 소울 푸드는?, content=댓글 ㄱㄱ), nickname=Park, body=치킨), Comment(id=7, article=Article(id=6, title=당신의 취미는?, co ...

테스트 전체 코드

@DataJpaTest // JPA와 연동한 테스트!
class CommentRepositoryTest {

    @Autowired
    CommentRepository commentRepository;

    @Test
    @DisplayName("특정 게시글의 모든 댓글 조회")
    void findByArticleId() {
        /* Case 1: 4번 게시글의 모든 댓글 조회 */
        {
            // 입력 데이터 준비
            Long articleId = 4L;

            // 실제 수행
            List<Comment> comments = commentRepository.findByArticleId(articleId);

            // 예상하기
            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);

            // 검증
            assertEquals(expected.toString(), comments.toString(), "4번 글의 모든 댓글을 출력!");
        }

        /* Case 2: 1번 게시글의 모든 댓글 조회 */
        {
            // 입력 데이터 준비
            Long articleId = 1L;

            // 실제 수행
            List<Comment> comments = commentRepository.findByArticleId(articleId);

            // 예상하기
            Article article = new Article(1L, "가가가가가", "11111");
            List<Comment> expected = Arrays.asList();

            // 검증
            assertEquals(expected.toString(), comments.toString(), "1번 글은 댓글이 없음");
        }
    }

    @Test
    @DisplayName("특정 닉네임의 모든 댓글 조회")
    void findByNickname() {
        /* Case 1: "Park"의 모든 댓글 조회 */
        {
            // 입력 데이터를 준비
            String nickname = "Park";
            // 실제 수행
            List<Comment> comments = commentRepository.findByNickname(nickname);
            // 예상하기
            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);
            // 검증
            assertEquals(expected.toString(), comments.toString(), "Park의 모든 댓글을 출력!");
        }
    }
}

 

 

728x90
반응형