Skip to content

[테크블로그] 무한 대댓글

MoonMinHyuk1 edited this page Jun 5, 2023 · 3 revisions

ERD 구조

스크린샷 2023-06-05 오전 3 56 06

all_children_size

  • 해당 댓글에 해당하는 모든 하위 댓글의 수입니다.

level

  • 댓글의 계층을 나타냅니다. (댓글 : 1, 대댓글 : 2, 대대댓글 : 3, …)

ref

  • 최상위 댓글의 고유 번호입니다.

step

  • 댓글, 대댓글, 대대댓글 등 계층에 상관없이 위에서부터의 순서를 나타냅니다.
  • 댓글 조회 시 정렬에 사용됩니다.

parent_id

  • 해당 댓글의 부모 댓글 아이디입니다.
  • 최상위 댓글의 경우 null값이 저장됩니다.

댓글 예시

스크린샷 2023-06-06 오전 2 20 50

데이터베이스 저장 방식

@Transactional
override fun saveRecordComment(recordCommentReqDto: RecordCommentReqDto, user: User): Long {
    val record = recordRepository.findByIdOrThrow(recordCommentReqDto.recordId)
    var parent: RecordComment? = null
    val refCount = recordCommentRepository.findNextRef(record)
    var ref: Long = (refCount + 1).toLong()
    var step: Long = 1
    var level: Long = 1
    val allChildrenSize: Long = 0
    if(recordCommentReqDto.parentId != null) {
        parent = recordCommentRepository.findByIdOrThrow(recordCommentReqDto.parentId)

        val parentStep: Long = parent.step
        val parentAllChildrenSize: Long = parent.allChildrenSize
        step = parentStep + parentAllChildrenSize + 1
        ref = parent.ref
        level = parent.level + 1

        val comments = recordCommentRepository.findSameRef(record, ref)
        updateAllChildrenSize(parent)
        updateStep(comments, step)
    }
    return recordCommentRepository.save(
        RecordComment(recordCommentReqDto.content, record, user, ref, step, level, allChildrenSize, parent)
    ).id
}

parent_id가 없는 경우 (최상위 댓글인 경우)

  • parent 에 null을 저장합니다. (parent_id에는 null값이 저장)
  • step, level에 1, all_children_size에 0을 저장합니다.
  • ref는 최상위 댓글이 가지는 고유 번호이기 때문에 record의 댓글 중 최대 값의 +1 해준 값을 저장합니다.

parent_id가 있는 경우 (하위 댓글인 경우)

  • parent에 부모 댓글의 객체를 저장합니다. (parent_id에는 parent의 id값이 저장)

  • step에는 (부모 댓글의 step 값) + (부모 댓글의 모든 하위 댓글 수) +1 해준 값을 저장합니다. 부모 댓글의 모든 하위 댓글 다음에 위치해야 하기 때문입니다.

  • ref는 최상위 댓글이 가지는 고유 번호이기 때문에 부모 댓글의 ref값을 저장합니다.

  • level은 부모 댓글보다 한단계 깊은 계층이기 때문에 부모 댓글 level에 +1 해준 값을 저장합니다.

  • 하위 댓글은 댓글의 어디 들어가든 간에 ref가 같은 댓글 중 상당 수의 댓글의 all_children_size나 step값을 변경해주어야 합니다. 그렇기 때문에 같은 ref를 가지는 댓글을 전체 조회합니다.

  • ref가 같은 댓글 중 parent댓글을 타고 올라가면서 해당 댓글의 all_children_size를 +1 해줍니다.

    private fun updateAllChildrenSize(parent: RecordComment?) {
        var grandParent = parent
        while(grandParent != null) {
            grandParent!!.updateAllChildrenSize()
            grandParent = grandParent!!.parent
        }
    }
    
  • ref가 같은 댓글 중 등록될 댓글의 step 값 이상인 댓글의 step을 +1 해줍니다.

    private fun updateStep(comments: List<RecordComment>, step: Long) {
        comments.forEach {
            if(it.step >= step) {
                it.updateStep()
            }
        }
    }
    

전체 댓글 조회

override fun findRecordComments(record: Record, pageable: Pageable): Page<RecordCommentResDto> {
    val content = jpaQueryFactory
        .select(QRecordCommentResDto(recordComment.id, recordComment.level, recordComment.content, user.nickName, recordComment.createdAt))
        .from(recordComment)
        .leftJoin(recordComment.user, user)
        .where(recordComment.record.eq(record))
        .orderBy(
            recordComment.ref.asc(),
            recordComment.step.asc()
        )
        .offset(pageable.offset)
        .limit(pageable.pageSize.toLong())
        .fetch()

    val countQuery = jpaQueryFactory
        .select(QRecordCommentResDto(recordComment.id, recordComment.level, recordComment.content, user.nickName, recordComment.createdAt))
        .from(recordComment)
        .leftJoin(recordComment.user, user)
        .where(recordComment.record.eq(record))

    return PageableExecutionUtils.getPage(content, pageable) { countQuery.fetch().size.toLong() }
}
  • 하나의 최상위 댓글과 그에 해당하는 모든 하위 댓글을 먼저 보여주어야 하기 때문에 ref를 기준으로 먼저 오름차순 정렬합니다.
  • 댓글이 저장될 때 step을 계산해서 저장하기 때문에 같은 ref 중 step을 오름차순으로 정렬된 결과를 조회합니다.
  • 추가로, count query의 효율성을 높이기 위해 PageableExecutionUtils를 이용해 필요한 경우에만 count query를 호출하도록 설정했습니다. 이를 사용하면 조회한 결과가 page size보다 작거나, 마지막 페이지일 때는 count query를 호출하지 않습니다.
Clone this wiki locally