본문 바로가기
💻 뚝딱뚝딱/북북클럽

[개발일지 #022] 댓글(Comment) 도메인 구현 및 단위테스트

by 뚜루리 2025. 4. 30.
728x90
320x100

🎯 오늘의 목표

  • 댓글(Comment) 도메인 개발

⚙️ 진행한 작업

  • 댓글(Comment) 엔티티 생성
  • 댓글(Comment) 레파지토리 생성
  • 댓글(Comment)  서비스 생성
  • 댓글(Comment) 관련 예외 생성
  • 댓글(Comment) 도메인 단위테스트

🛠️ 개발내용

📌  댓글(Comment) 엔티티 생성

package ddururi.bookbookclub.domain.comment.entity;

import java.time.LocalDateTime;

/**
 * 댓글(Comment) 엔티티
 * - 사용자가 특정 피드에 작성한 댓글을 저장
 * - 피드(Feed)와 댓글(Comment)은 다대일 관계
 * - 대댓글은 지원하지 않음
 */
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class)
public class Comment {

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

    /** 댓글 내용 */
    @Column(nullable = false, columnDefinition = "TEXT")
    private String content;

    /** 댓글 작성자 */
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)
    private User user;

    /** 댓글이 작성된 피드 */
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "feed_id", nullable = false)
    private Feed feed;

    /** 댓글 좋아요 수 */
    @Column(nullable = false)
    private int likeCount = 0;

    /** 댓글 생성일 */
    @CreatedDate
    @Column(nullable = false, updatable = false)
    private LocalDateTime createdAt;

    /** 댓글 수정일 */
    @LastModifiedDate
    private LocalDateTime updatedAt;

    /**
     * 댓글 생성 정적 메서드
     */
    public static Comment create(String content, User user, Feed feed) {
        Comment comment = new Comment();
        comment.content = content;
        comment.user = user;
        comment.feed = feed;
        return comment;
    }

    /**
     * 댓글 내용 수정
     */
    public void updateContent(String content) {
        this.content = content;
    }

}

 

📌  댓글(Comment) 레파지토리 생성

package ddururi.bookbookclub.domain.comment.repository;

import ddururi.bookbookclub.domain.comment.entity.Comment;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

/**
 * 댓글(Comment) 레포지토리
 * - 댓글 CRUD 및 피드 ID 기준 댓글 조회 기능 제공
 */
public interface CommentRepository extends JpaRepository<Comment, Long> {

    /**
     * 특정 피드 ID에 작성된 댓글 목록 조회 (최신순 정렬)
     * @param feedId 피드 ID
     * @return 댓글 리스트
     */
    List<Comment> findByFeedIdOrderByCreatedAtDesc(Long feedId);
}

 

📌  댓글(Comment) 서비스 생성

package ddururi.bookbookclub.domain.comment.service;


/**
 * 댓글(Comment) 서비스
 * - 댓글 등록, 조회, 삭제 기능 제공
 */
@Service
@RequiredArgsConstructor
@Transactional
public class CommentService {

    private final CommentRepository commentRepository;
    private final FeedRepository feedRepository;

    /**
     * 댓글 등록
     * @param content 댓글 내용
     * @param user 작성자
     * @param feedId 댓글이 달릴 피드 ID
     * @return 저장된 댓글
     */
    public Comment createComment(String content, User user, Long feedId) {
        Feed feed = feedRepository.findById(feedId)
                .orElseThrow(FeedNotFoundException::new);

        Comment comment = Comment.create(content, user, feed);
        return commentRepository.save(comment);
    }

    /**
     * 특정 피드에 작성된 댓글 목록 조회 (최신순)
     * @param feedId 피드 ID
     * @return 댓글 리스트
     */
    public List<Comment> getCommentsByFeed(Long feedId) {
        return commentRepository.findByFeedIdOrderByCreatedAtDesc(feedId);
    }

    /**
     * 댓글 삭제
     * @param commentId 댓글 ID
     * @param user 삭제 요청 사용자
     */
    public void deleteComment(Long commentId, User user) {
        Comment comment = commentRepository.findById(commentId)
                .orElseThrow(CommentNotFoundException::new);

        if (!comment.getUser().getId().equals(user.getId())) {
            throw new CommentAccessDeniedException();
        }

        commentRepository.delete(comment);
    }
}

 

📌  댓글(Comment) 관련예외 생성

CommentAccessDeniedException.java

package ddururi.bookbookclub.domain.comment.exception;

import ddururi.bookbookclub.global.exception.ErrorCode;
import lombok.Getter;

/**
 * 댓글 삭제 권한이 없는 경우 발생
 */
@Getter
public class CommentAccessDeniedException extends RuntimeException {

    private final ErrorCode errorCode;

    public CommentAccessDeniedException() {
        super(ErrorCode.COMMENT_ACCESS_DENIED.getMessage());
        this.errorCode = ErrorCode.COMMENT_ACCESS_DENIED;
    }
}

 

CommentNotFoundException.java

package ddururi.bookbookclub.domain.comment.exception;

import ddururi.bookbookclub.global.exception.ErrorCode;
import lombok.Getter;

/**
 * 존재하지 않는 댓글에 접근할 때 발생
 */
@Getter
public class CommentNotFoundException extends RuntimeException {

    private final ErrorCode errorCode;

    public CommentNotFoundException() {
        super(ErrorCode.COMMENT_NOT_FOUND.getMessage());
        this.errorCode = ErrorCode.COMMENT_NOT_FOUND;
    }
}

 

 

 

📌  댓글(Comment) 도메인 단위테스트

package ddururi.bookbookclub.domain.comment.service;

import ddururi.bookbookclub.domain.comment.entity.Comment;
import ddururi.bookbookclub.domain.comment.exception.CommentAccessDeniedException;
import ddururi.bookbookclub.domain.comment.exception.CommentNotFoundException;
import ddururi.bookbookclub.domain.comment.repository.CommentRepository;
import ddururi.bookbookclub.domain.feed.entity.Feed;
import ddururi.bookbookclub.domain.feed.exception.FeedNotFoundException;
import ddururi.bookbookclub.domain.feed.repository.FeedRepository;
import ddururi.bookbookclub.domain.user.entity.User;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.test.util.ReflectionTestUtils;

import java.util.List;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

class CommentServiceTest {

    @Mock
    private CommentRepository commentRepository;

    @Mock
    private FeedRepository feedRepository;

    @InjectMocks
    private CommentService commentService;

    public CommentServiceTest() {
        MockitoAnnotations.openMocks(this);
    }

    @Nested
    @DisplayName("댓글 등록 테스트")
    class CreateCommentTest {

        @Test
        @DisplayName("댓글 등록 성공")
        void createComment_success() {
            // given
            User user = User.create("test@email.com", "encodedPassword", "nickname");
            Feed feed = mock(Feed.class);
            when(feedRepository.findById(1L)).thenReturn(Optional.of(feed));
            when(commentRepository.save(any(Comment.class))).thenAnswer(invocation -> invocation.getArgument(0));

            // when
            Comment comment = commentService.createComment("테스트 댓글", user, 1L);

            // then
            assertThat(comment.getContent()).isEqualTo("테스트 댓글");
            assertThat(comment.getUser()).isEqualTo(user);
            assertThat(comment.getFeed()).isEqualTo(feed);
        }

        @Test
        @DisplayName("댓글 등록 실패 - 피드 없음")
        void createComment_feedNotFound() {
            // given
            User user = User.create("test@email.com", "encodedPassword", "nickname");
            when(feedRepository.findById(1L)).thenReturn(Optional.empty());

            // when & then
            assertThatThrownBy(() -> commentService.createComment("테스트 댓글", user, 1L))
                    .isInstanceOf(FeedNotFoundException.class);
        }
    }

    @Nested
    @DisplayName("댓글 조회 테스트")
    class GetCommentsByFeedTest {

        @Test
        @DisplayName("피드 ID로 댓글 조회 성공")
        void getCommentsByFeed_success() {
            // given
            Feed feed = mock(Feed.class);
            List<Comment> comments = List.of(
                    Comment.create("댓글1", User.create("user1@email.com", "pass", "nickname1"), feed),
                    Comment.create("댓글2", User.create("user2@email.com", "pass", "nickname2"), feed)
            );
            when(commentRepository.findByFeedIdOrderByCreatedAtDesc(1L)).thenReturn(comments);

            // when
            List<Comment> result = commentService.getCommentsByFeed(1L);

            // then
            assertThat(result).hasSize(2);
            assertThat(result.get(0).getContent()).isEqualTo("댓글1");
            assertThat(result.get(1).getContent()).isEqualTo("댓글2");
        }
    }

    @Nested
    @DisplayName("댓글 삭제 테스트")
    class DeleteCommentTest {

        @Test
        @DisplayName("댓글 삭제 성공")
        void deleteComment_success() {
            // given
            User user = User.create("test@email.com", "encodedPassword", "nickname");
            ReflectionTestUtils.setField(user, "id", 1L);

            Feed feed = mock(Feed.class);
            Comment comment = Comment.create("삭제할 댓글", user, feed);
            ReflectionTestUtils.setField(comment, "id", 1L);

            when(commentRepository.findById(1L)).thenReturn(Optional.of(comment));

            // when
            commentService.deleteComment(1L, user);

            // then
            verify(commentRepository, times(1)).delete(comment);
        }


        @Test
        @DisplayName("댓글 삭제 실패 - 댓글 없음")
        void deleteComment_commentNotFound() {
            // given
            when(commentRepository.findById(1L)).thenReturn(Optional.empty());

            // when & then
            assertThatThrownBy(() -> commentService.deleteComment(1L,
                    User.create("test@email.com", "encodedPassword", "nickname")))
                    .isInstanceOf(CommentNotFoundException.class);
        }

        @Test
        @DisplayName("댓글 삭제 실패 - 권한 없음")
        void deleteComment_accessDenied() {
            // given
            User owner = User.create("owner@email.com", "encodedPassword", "ownerNick");
            User otherUser = User.create("other@email.com", "encodedPassword", "otherNick");
            ReflectionTestUtils.setField(owner, "id", 1L);
            ReflectionTestUtils.setField(otherUser, "id", 2L);

            Feed feed = mock(Feed.class);
            Comment comment = Comment.create("권한 없는 삭제 시도", owner, feed);

            when(commentRepository.findById(1L)).thenReturn(Optional.of(comment));

            // when & then
            assertThatThrownBy(() -> commentService.deleteComment(1L, otherUser))
                    .isInstanceOf(CommentAccessDeniedException.class);
        }

    }
}
728x90
320x100