본문 바로가기
프로젝트/Recipository

[Dev] 22.12.15. 게시글 조회 (+ 댓글)

by 규글 2022. 12. 15.

 게시글에 대한 Recipe Entity와 댓글에 대한 Comment Entity를 지난 게시글에서 양방향 연관 관계로 구성했다고 언급했다. 지난 게시글로 돌아가기는 번거로울테니 각 Entity는 전체의 내용을 가져왔다.

 

Recipe.java

package com.example.recipository.domain;

import com.example.recipository.dto.CommentDto;
import com.example.recipository.dto.RecipeDto;
import lombok.*;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;

@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Table(name = "recipe")
@Entity
public class Recipe extends BaseTime {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "content_id")
    private Long contentId;
    private String title;
    private String writer;
    private String content;
    private String imagePath;
    private Long viewCount;
    private Long likeCount;
    @Transient
    private List<Link> link;
    private String category;
    private boolean bePublic;
    @OneToMany(mappedBy = "recipe", fetch = FetchType.LAZY)
    private List<Comment> commentList;
    @Column(nullable = false)
    private LocalDateTime modDate;

    @Override
    public LocalDateTime getRegDate() {
        return super.getRegDate();
    }

    public RecipeDto toDto(){
        List<String> linkList = new ArrayList<String>();
        if(link != null){
            link.forEach(tmp -> {
                linkList.add(tmp.getLink());
            });
        }

        return RecipeDto.builder()
                .contentId(contentId)
                .title(title)
                .writer(writer)
                .content(content)
                .imagePath(imagePath)
                .viewCount(viewCount)
                .likeCount(likeCount)
                .link(linkList)
                .category(category)
                .bePublic(bePublic)
                .regDate(getRegDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
                .modDate(getModDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
                .build();
    }

    public void setRecipeAtLink(){
        link.forEach(tmp -> {
            tmp.setRecipe(this);
        });
    }

    public List<CommentDto> getCommentDtoList(){
        List<CommentDto> commentDtoList = new ArrayList<CommentDto>();
        this.commentList.forEach(tmp -> {
            commentDtoList.add(tmp.toDto());
        });

        return commentDtoList;
    }
}

 

Comment.java

package com.example.recipository.domain;

import com.example.recipository.dto.CommentDto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.time.format.DateTimeFormatter;

@Getter
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "comment")
@Entity
@Builder
public class Comment extends BaseTime {
    @Id
    private Long commentId;
    private String writer;
    private String comment;
    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
    @JoinColumn(name = "target_id")
    private Recipe recipe;
    private Long groupId;

    public CommentDto toDto(){
        return CommentDto.builder()
                .commentId(this.commentId)
                .writer(this.writer)
                .comment(this.comment)
                .groupId(this.groupId)
                .regDate(super.getRegDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
                .build();
    }
}

 

 

RecipeServiceImpl.java

    // 게시글의 data를 가져오는 service logic
    @Override
    public Map<String, Object> getRecipe(Long contentId, Cookie[] cookieList) {
        (...)
        
        // link repository로부터 가져온 List<Link>
        List<Link> linkList = linkRepository.findAllByRecipe(recipe);

        // List<Link> 의 한 Link로부터의 온전한 Recipe entity를 Dto로
        recipe = linkList.get(0).getRecipe();
        RecipeDto recipeDto = recipe.toDto();

        List<CommentDto> commentDtoList = recipe.getCommentDtoList();
        
        (...)
        
        map.put("commentDtoList", commentDtoList);

 이전에 Recipe Entity를 Link로부터 거꾸로 조회했다면, Comment는 Recipe와 양방향 연관 관계이기 때문에 Recipe로부터 직접 조회가 가능하다는 차이가 있다. Recipe로부터 조회하는 형상도 훨씬 직관적이라서 아무리 얻게되는 결과가 같다고 하더라도 N:1 단방향 연관 관계보다는 양방향 연관 관계로 구성해서 직관적으로 data를 얻어오는 것이 더 나아보인다.

 

2022.12.04 - [프로젝트/Recipository] - [Dev] 22.12.04. 게시글 작성 항목 중 링크에 대하여(2) : 작업

 

[Dev] 22.12.04. 게시글 작성 항목 중 링크에 대하여(2) : 작업

사실 직전 게시글에서 연관 관계에 대한 이런 저런 내용을 다뤘지만 사실 게시글에 작성하는 link 정보는 양방향 관계가 필요 없었다. 게시글은 작성한 link의 정보가 필요하지만, link 정보는 자신

gyuggling.tistory.com

 사실 직전 게시글에서 연관 관계에 대한 이런 저런 내용을 다뤘지만 사실 게시글에 작성하는 link 정보는 양방향 관계가 필요 없었다. 게시글은 작성한 link의 정보가 필요하지만, link 정보는 자신이 어떤 게시글에 작성되었는지에 대한 정보가 필요 없다는 말이다.

 얼마 전 작성했던 위 게시글에 대한 작업을 기록하면서 가장 먼저 언급한 내용이다. 그런데 말이 조금 이상하다. 게시글에서는 작성한 링크의 정보가 필요하고, 링크에서는 어떤 게시물에 작성되었는지 정보가 필요 없다면 그것은 게시글과 링크 사이에 1:N 단방향 연관 관계를 구성해야한다는 의미가 된다.

 하지만 1:N 단방향 연관 관계로 구성하면 N쪽에 대한 update query가 사실상 강제되는 문제가 있어서 N:1 단방향 연관 관계로 구성한 것이다. 더하여 위 언급에 따르면 게시글에서는 링크 정보를 알아야하고, 마찬가지로 게시글에서는 댓글 정보를 알아야하기 때문에 1:N 단방향 연관 관계 또한 존재해야한다. 결국 양방향 연관 관계로 구성했어야하는 것이 맞는 수순이었던 것인지에 대한 생각을 다시 하게 된다. 더 직관적인 코드를 작성할 수 있다는 점 또한 메리트이기도 하고, 지인이 보여준 양방향 연관 관계로 구성한 Entity의 이미지를 본 적이 있기도 하다.

 그래도 해둔 작업은 비교를 위해서 그대로 남겨두기로 한다.

 

 그런데 이대로 출력하게 되면 위 이미지처럼 댓글이 출력된다. 1부터 5까지 순서대로 출력되었는데 어떤 문제가 있는가 하면, 순서대로 작성된 것은 맞지만 3과 4는 1번 댓글에 대한 대댓글이라는 점이다. 즉, DB로부터 CommentList를 가져올 때 sort가 되지 않고 있다.

 

    List<Comment> findAllByRecipeOrderByGroupIdAscCommentIdAsc(Recipe recipe);

(CommentRepository.java)
-------------------------------------------------------------------------------
(RecipeServiceImpl.java)

//        List<CommentDto> commentDtoList = recipe.getCommentDtoList();
        List<Comment> commentList = commentRepository.findAllByRecipeOrderByGroupIdAscCommentIdAsc(recipe);
        List<CommentDto> commentDtoList = new ArrayList<>();
        commentList.forEach(tmp -> {
            commentDtoList.add(tmp.toDto());
        });

 그래서 data의 sort를 위해서 JPA의 method naming하는 방식을 사용했다. 그리고 sorting 후의 Entity List를 Dto List로 변환하여 controller로 전달하는 방식을 택했다.

  • findAll : 모두 조회하는데
  • ByRecipe : Recipe에 있는 content id 즉, target id를 기준으로
  • OrderBy : Ordering을 하는데
  • GroupIdAsc : GroupId를 첫 기준으로 Ascending(순차)
  • CommentIdAsc : CommentId를 두 번째 기준으로 Ascending(순차)

 

 그런데 이 모습은 게시글의 data를 가져오면서 댓글의 data를 가져오는, 양방향 연관 관계로 구성한 이유를 상실하는 것이라는 생각을 하게 했다. 그럴 즈음에 한 블로그를 발견했다.[각주:1]

 

    @OneToMany(mappedBy = "recipe", fetch = FetchType.LAZY)
    @OrderBy("groupId asc, commentId asc")
    private List<Comment> commentList;

 해당 블로그에서는 @OrderBy annotation을 소개하고 있는데, 해당 annotation 안에 query의 order by 절에 필요한 조건을 넣어주면 작성한대로 List가 sorting된다. 아래 이미지와 같이 출력되는 것이 필자가 의도한 바이며, JPA method도 좋지만 연관 관계 구성 의도에 맞는 것은 @OrderBy annotation을 사용하는 것이라고 생각한다.

 

 

content.html

    <div class="comment" id="commentDiv">
        <h2 class="row col-8">댓글</h2>
        <div class="row col-8" th:each="comment : ${commentList}">
            <div th:if="${comment.groupId} == ${comment.commentId}">
                <div th:replace="fragments/comment :: normal(${comment})"></div>
            </div>
            <div th:if="${comment.groupId} != ${comment.commentId}">
                <div th:replace="fragments/comment :: reply(${comment})"></div>
            </div>
        </div>
    </div>

 이전 댓글 작성에 이미 포함된 내용이지만 댓글의 내용을 출력하는 부분만을 가져왔다. Server로부터 넘겨받은 commentList를 each로 반복하여 그 안의 comment id와 group id의 관계에 따라 서로 다른 template을 출력하도록 한 것이다. Template에는 each로 가져오는 List 하나하나의 data를 넘겨준다.

 

conmment.html

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<div th:fragment="normal(comment)"
     th:class="|row reply groupId${comment.groupId}|"
     th:id="|normalComment${comment.commentId}|">
    <div class="flex-row">
        <span th:text="${comment.writer}"></span>
        <span th:text="|/ ${comment.regDate}|"></span>
    </div>
    <input type="text" class="form-control col"
           style="background-color: white;" th:value="${comment.comment}" disabled>
    <button th:data-num="${comment.groupId}"
            th:data-text="${comment.writer}"
            sec:authorize="isAuthenticated()"
            class="btn btn-light float-end col-1 replyBtn" type="submit"
            style="width: 70px;">답글</button>
    <button th:if="${comment.writer} == ${#authentication.name}"
            class="btn btn-light float-end col-1 deleteBtn" type="submit"
            style="width: 70px;">삭제</button>
</div>
<div th:fragment="reply(comment)"
     th:class="|row reply groupId${comment.groupId}|"
     th:id="|replyComment${comment.commentId}|">
    <svg xmlns="http://www.w3.org/2000/svg" width="29" height="29"
         fill="currentColor" class="bi bi-arrow-return-right col-1" viewBox="0 0 16 16"
         style="margin-top: 25px;">
        <path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 0 1 2v4.8a2.5 2.5 0 0 0 2.5
                    2.5h9.793l-3.347 3.346a.5.5 0 0 0 .708.708l4.2-4.2a.5.5 0 0 0 0-.708l-4-4a.5.5
                    0 0 0-.708.708L13.293 8.3H3.5A1.5 1.5 0 0 1 2 6.8V2a.5.5 0 0 0-.5-.5z"/>
    </svg>
    <div class="row col" style="padding: 0px;">
        <div class="flex-row">
            <span th:text="${comment.writer}"></span>
            <span th:text="|/ ${comment.regDate}|"></span>
        </div>
        <input type="text" id="" name="" class="form-control col"
               style="background-color: white;" th:value="${comment.comment}" disabled>
        <button th:data-num="${comment.groupId}"
                th:data-text="${comment.writer}"
                sec:authorize="isAuthenticated()"
                class="btn btn-light float-end col-1 replyBtn" type="submit"
                style="width: 70px;">답글</button>
        <button th:if="${comment.writer} == ${#authentication.name}"
                class="btn btn-light float-end col-1" type="submit"
                style="width: 70px;">삭제</button>
    </div>
</div>
</html>

 게시글에 대한 댓글과, 댓글에 대한 댓글을 구분하여 template을 만든 것이다. 게시글 출력에서 상황에 맞게 다른 template을 replace해서 사용할 수 있도록 했다.

 

 이렇게 댓글을 출력하는 것까지 기록했다. 이제 Dto와 Entity에 대한 게시글을 작성한 뒤에 댓글을 삭제하는 기능, 게시글을 수정하고 삭제하는 기능을 작업할 예정이다.

 

Reference

댓글