[빙터뷰] 댓글 기능 구현

impala·2023년 4월 7일
0
post-thumbnail

Comment

사용자가 영상 게시판에 면접 연습 영상을 올리면 다른 사용자들이 영상을 보고 피드백을 해주는 기능이다. 일반적인 게시판의 댓글 기능과 유사하다.

Domain

Comment

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Comment  extends EntityDate {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "comment_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "board_id")
    private Board board;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;
    private String content;

    @OneToMany(mappedBy = "comment", cascade = CascadeType.REMOVE)
    private List<CommentMemberLike> likes;


    @Builder
    public Comment(Board board, Member member, String content) {
        this.board = board;
        this.member = member;
        this.content = content;
    }

    public void update(String content) {
        this.content = content;
    }
}

데이터베이스의 Comment테이블에 해당하는 엔티티이다. id값은 DB에서 자동으로 생성되도록 Identity전략을 사용하여 위임하였고, jpql의 n+1문제를 방지하기 위해 모든 연관관계에 지연로딩 전략을 사용하였다. 또한 댓글에 달린 좋아요를 편리하게 확인하기 위해 CommentMemberLike테이블을 참조하는 likes필드를 만들어 매핑하였다. 또한 comment가 삭제되면 해당 comment에 좋아요를 누른 기록이 모두 삭제될 수 있도록 likes필드에 CascadeType.REMOVE옵션을 걸어두었다.

무분별한 생성과 수정을 막기 위해 Setter는 닫아두었으며 생성자를 통해서 초기값을 설정하고 update메소드를 통해서만 값을 변경할 수 있도록 구현하였다. 다만 스프링에서 프록시를 사용하기 위해서는 기본생성자가 필요하기 때문에 기본생성자의 접근권한을 protected로 제한하였다.

CommentMemberLike

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class CommentMemberLike {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "comment_member_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "comment_id")
    private Comment comment;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;

    @Enumerated(EnumType.STRING)
    private LikeType likeStatus;

    @Builder
    public CommentMemberLike(Comment comment, Member member) {
        this.comment = comment;
        this.member = member;
        this.likeStatus = LikeType.LIKE;
    }

    public void updateStatus() {
        if (this.likeStatus == LikeType.LIKE) {
            this.likeStatus = LikeType.UNLIKE;
        }else{
            this.likeStatus = LikeType.LIKE;
        }
    }
}

댓글 좋아요 정보를 관리하는 엔티티로 Member와 Comment를 연결하는 정보를 담고 있다. 또한 likeStatus를 두어 LIKE상태인지 UNLIKE상태인지 관리한다.

DTO

RequestDTO

CommentCreateDTO

package ving.vingterview.dto.comment;

import lombok.Data;

@Data
public class CommentCreateDTO {

    private Long boardId;
    private Long memberId;
    private String content;
}

클라이언트에서 comment를 생성할 때 전달받는 DTO이다. DTO자체는 비즈니스적으로 의미있는 객체가 아니기 때문에 편의상 @Data를 통해 Getter와 Setter를 모두 열어두었다.

현재는 로그인 기능이 구현되지 않은 상태이기 때문에 작성자의 memberId를 직접 받지만, 추후 로그인 및 인증기능을 개발하면 세션에서 요청을 보낸 사용자 정보를 얻어오는 방식으로 교체할 계획이다.

CommentUpdateDTO

package ving.vingterview.dto.comment;

import lombok.Data;

@Data
public class CommentUpdateDTO {

    private String content;
}

클라이언트에서 comment를 수정할 때 전달받는 DTO로, comment의 내용 외에는 수정할 수 없다.

ResponseDTO

CommentDTO

@Data
@NoArgsConstructor
public class CommentDTO {

    private Long commentId;
    private Long boardId;
    private Long memberId;
    private String memberNickname;
    private String profileImageUrl;
    private String content;
    private int likeCount;

    @Builder
    public CommentDTO(Long commentId, Long boardId, Long memberId, String memberNickname, String profileImageUrl, String content, int likeCount) {
        this.commentId = commentId;
        this.boardId = boardId;
        this.memberId = memberId;
        this.memberNickname = memberNickname;
        this.profileImageUrl = profileImageUrl;
        this.content = content;
        this.likeCount = likeCount;
    }
}

클라이언트가 comment를 조회할 때 서버에서 전달하는 DTO로, comment의 정보뿐만 아니라 comment가 달린 게시글의 정보, comment를 작성한 사용자의 정보가 추가로 넘어간다. 멤버필드의 수가 많기 때문에 @Builder어노테이션을 통해 객체를 생성할 때의 편의성을 높였다.

CommentListDTO

package ving.vingterview.dto.comment;

import lombok.Data;

import java.util.List;

@Data
@AllArgsConstructor
public class CommentListDTO {

    private List<CommentDTO> comments;
}

클라이언트에서 여러개의 comment를 조회할 때 서버에서 전달하는 DTO로, 앞서 설명한 CommentDTO를 List로 담아 전달한다.

Repository

CommentRepository

public interface CommentRepository extends JpaRepository<Comment,Long> {

    public List<Comment> findAllByBoard(Board board);
    public List<Comment> findAllByMember(Member member);
}

개발의 생산성을 높이기 위해 SpringDataJpa를 사용하여 기본적인 메소드들을 상속받았다. 또한 서비스단에서 여러 comment들을 작성자와 게시글을 기준으로 필터링하여 조회하는 기능이 필요해서 findAllByBoard, findAllByMember 두 메소드를 추가하였다.

CommentMemberLikeRepository

public interface CommentMemberLikeRepository extends JpaRepository<CommentMemberLike,Long> {

    Optional<CommentMemberLike> findByCommentIdAndMemberId(Long commentId, Long memberId);

    int countByCommentAndLikeStatus(Comment comment, LikeType status);

}

특정 사용자가 특정 댓글에 좋아요를 눌렀는지 확인하기 위해 findByCommentIdAndMemberId에서는 (댓글id, 회원id)쌍으로 데이터를 조회한다. 이때 한번도 좋아요를 누르지 않았다면 데이터가 없기 때문에 Optional로 감싸서 반환한다.

또한 사용자가 댓글을 조회하면 좋아요 수가 같이 전달되어야 하기 때문에 테이블에서 해당 댓글의 좋아요 수를 세는 countByCommentAndLikeStatus메소드를 추가하였다.

Service

member fields

@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class CommentService {

    private final CommentRepository commentRepository;
    private final BoardRepository boardRepository;
    private final MemberRepository memberRepository;
    ...
}

jpa를 사용하기 위해 @Transactional 어노테이션을 통해 서비스단에 트랜잭션을 걸어두었다. CommentService에서는 comment가 달린 게시글과 작성자의 정보를 찾기 위해 commentRepository외에도 memberRepository와 boardRepository를 추가로 사용한다. 레포지토리 빈들은 private final키워드와 @RequiredArgsConstructor를 사용하여 스프링으로부터 주입받는다

create

	/**
     * 댓글 등록
     * @param commentCreateDTO
     * @return
     */
    public Long create(CommentCreateDTO commentCreateDTO) {
        Board board = boardRepository.findById(commentCreateDTO.getBoardId())
                .orElseThrow(()->new NoSuchElementException("게시글 없음"));
        Member member = memberRepository.findById(commentCreateDTO.getMemberId())
                .orElseThrow(()->new NoSuchElementException("회원 없음"));
        String content = commentCreateDTO.getContent();

        Comment comment = new Comment(board, member, content);

        return commentRepository.save(comment).getId();
    }

comment를 생성하는 메소드로, 컨트롤러로부터 DTO를 받아 board와 member를 조회한 뒤 comment객체를 생성하여 레포지토리에 저장하고, 생성된 comment의 id값을 반환한다.

만약 DTO에 담겨온 id값으로 board와 member를 찾을 수 없는 경우, 일단 NoSuchElementException을 던지도록 구현하였다. 예외처리에 대한 내용은 추후에 보완할 계획이다.

update

	/**
     * 댓글 수정
     * @param id
     * @param commentUpdateDTO
     * @return
     */
    public Long update(Long id, CommentUpdateDTO commentUpdateDTO) {
        Comment comment = commentRepository.findById(id)
                .orElseThrow(()->new NoSuchElementException("댓글 없음"));
        comment.update(commentUpdateDTO.getContent());
        return comment.getId();
    }

comment를 수정하는 메소드로 레포지토리에서 comment를 찾아 content를 업데이트한다.

delete

    /**
     * 댓글 삭제
     * @param id
     */
    public void delete(Long id) {
        commentRepository.deleteById(id);
    }

id로 comment를 찾아 삭제한다. deleteById메소드는 내부적으로 findById를 통해 comment를 찾은 뒤 값이 있으면 삭제하기 때문에 따로 반환값이 없다. 그래서 delete메소드에서도 아무 값도 반환하지 않지만 나중에 하다못해 삭제가 성공했다는 메세지라도 반환해야 할 것 같다.

findOne

	/**
     * 댓글 id로 댓글 1개 조회
     * @param id
     * @return
     */
    @Transactional(readOnly = true)
    public CommentDTO findOne(Long id) {
        Comment comment = commentRepository.findById(id)
                .orElseThrow(()->new NoSuchElementException("댓글 없음"));
        Member member = comment.getMember();
        Board board = comment.getBoard();

        return convertToCommentDTO(comment, member, board);
    }

id를 받아 comment의 정보를 반환하는 메소드이다. 조회 메소드이기 때문에 트랜잭션을 readOnly로 설정해주었다. 반환값은 ComentDTO 타입인데, comment정보 외에도 member와 board의 정보가 필요하기 때문에 각 레포지토리에서 데이터를 찾아 convertToCommentDTO메소드를 통해 DTO로 변환한다.

findByBoard

	/**
     * 게시글에 달린 댓글 조회
     * @param boardId
     * @return
     */
    @Transactional(readOnly = true)
    public CommentListDTO findByBoard(Long boardId) {
        Board board = boardRepository.findById(boardId)
                .orElseThrow(() -> new NoSuchElementException("게시글 없음"));
        List<Comment> comments = commentRepository.findAllByBoard(board);
        List<CommentDTO> results = comments.stream()
                .map((comment) -> {
                    Member member = comment.getMember();
                    return convertToCommentDTO(comment, member, board);
                })
                .toList();
        return new CommentListDTO(results);
    }

특정 게시글에 달린 comment를 보여줄 때 필요한 작업으로, 여러 comment를 boardId로 필터링하여 조회하는 메소드이다. 매개변수로 받은 boardId를 통해 board를 조회하고 레포지토리에서 board로 필터링한 comment를 리스트로 받는다. 이후 각각의 Comment객체를 CommentDTO로 변환하여 리스트로 받고, 결과로 나온 List<CommentDTO>를 CommentListDTO에 담아 반환한다.

findByMember

   /**
     * 회원이 작성한 댓글 조회
     * @param memberId
     * @return
     */
    @Transactional(readOnly = true)
    public CommentListDTO findByMember(Long memberId) {
        Member member = memberRepository.findById(memberId)
                .orElseThrow(() -> new NoSuchElementException("회원 없음"));
        List<Comment> comments = commentRepository.findAllByMember(member);
        List<CommentDTO> results = comments.stream()
                .map((comment) -> {
                    Board board = comment.getBoard();
                    return convertToCommentDTO(comment, member, board);
                })
                .toList();
        log.info("comments={}", results);
        return new CommentListDTO(results);
    }

비즈니스 요구사항에 모든 사용자들은 다른 사용자가 작성한 comment를 확인할 수 있도록 계획하였기 때문에 필요한 메소드로, 여러 comment를 memberId로 필터링하여 조회하는 메소드이다. 자세한 내용은 위와 같다.

like

	/**
     * 좋아요
     * @param id
     */
    public void like(Long id) {
        Long member_id = 1L; // 임시 회원
        Optional<CommentMemberLike> like = likeRepository.findByCommentIdAndMemberId(id, member_id);

        if (like.isEmpty()) {
            Comment comment = commentRepository.findById(id).orElseThrow(() -> new NoSuchElementException("해당 댓글을 찾을 수 없습니다."));
            Member member = memberRepository.findById(member_id).orElseThrow(() -> new NoSuchElementException("해당 멤버를 찾을 수 없습니다."));
            likeRepository.save(new CommentMemberLike(comment, member));

        } else {
            like.get().updateStatus();
        }
    }

댓글 좋아요 기능은 먼저 해당 댓글에 사용자가 좋아요를 누른 적이 있는지 확인하기 위해 like 중간 테이블에서 좋아요 기록을 조회한다. 만약 사용자가 해당 댓글에 처음 좋아요를 누른 상황이라면 좋아요 기록을 생성하여 댓글과 회원을 연결한 뒤 LIKE상태로 저장한다. 만약 회원이 한번 이상 좋아요를 눌렀다면 updateStatus메소드를 통해 좋아요 상태를 LIKE에서 UNLIKE로, 혹은 UNLIKE에서 LIKE로 바꾼다. 이렇게 구현한 이유는 사용자가 댓글에 좋아요를 누를 때마다 insert나 delete쿼리가 나가면 엔티티가 계속 생겼다가 삭제되는 현상이 반복되므로, likeStatus만 바꿔 엔티티를 재사용하려했기 때문이다. 또한 가능하면 UNLIKE상태의 엔티티는 일정 시간 이후 자공으로 삭제되게 구현하여 공간을 효울적으로 관리할 계획이다.

Controller

@Slf4j
@RestController
@RequestMapping("/comments")
@RequiredArgsConstructor
public class CommentController {

    private final CommentService commentService;

    @GetMapping(params = "board_id")
    public ResponseEntity<CommentListDTO> filterByBoard(@RequestParam(name = "board_id") Long boardId) {
        CommentListDTO result = commentService.findByBoard(boardId);
        return new ResponseEntity<>(result, HttpStatus.OK);
    }

    @GetMapping(params = "member_id")
    public ResponseEntity<CommentListDTO> filterByMember(@RequestParam(name = "member_id") Long memberId) {
        CommentListDTO result = commentService.findByMember(memberId);
        return new ResponseEntity<>(result, HttpStatus.OK);
    }

    @PostMapping(value = "")
    public Long create(@RequestBody CommentCreateDTO commentCreateDTO) {
        return commentService.create(commentCreateDTO);
    }

    @GetMapping("/{id}")
    public ResponseEntity<CommentDTO> comment(@PathVariable Long id) {
        CommentDTO comment = commentService.findOne(id);
        return new ResponseEntity<>(comment, HttpStatus.OK);
    }

    @DeleteMapping("/{id}")
    public void delete(@PathVariable Long id) {
        commentService.delete(id);
    }

    @PutMapping("/{id}")
    public Long update(@PathVariable Long id, @ModelAttribute CommentUpdateDTO commentUpdateDTO) {
        return commentService.update(id, commentUpdateDTO);
    }

    @GetMapping("/{id}/like")
    public void like(@PathVariable Long id) {
        commentService.like(id);
    }
}

컨트롤러에서는 기존에 설계한 API에 맞게 요청이 들어오면 commentService로 작업을 위임한다. @RestController어노테이션을 통해 컨트롤러를 스프링 빈에 등록함과 동시에 응답을 json형식으로 보내줄 수 있게 하였다. 또한 서비스계층과 동일하게 @RequiredArgsConstructor어노테이션을 통해 스프링으로부터 commentService 빈을 주입받는다.

아직 response에 대해서는 구체적으로 설계하지 않았기 때문에 API문서에 적은 대로 id값이나 DTO를 반환한다. 예외상황이나 리다이렉트등을 고려하여 response의 상태코드, 헤더, 메세지등을 추가할 계획이다.

Test

create, update, delete

	// 댓글 달기
	@Test
    void create() {
        CommentCreateDTO dto = new CommentCreateDTO();
        dto.setMemberId(1L);
        dto.setBoardId(1L);
        dto.setContent("첫번째 댓글입니다");

        Long commentId = commentService.create(dto);

        CommentDTO foundDto = commentService.findOne(commentId);

        assertThat(dto.getMemberId()).isEqualTo(foundDto.getMemberId());
        assertThat(dto.getBoardId()).isEqualTo(foundDto.getBoardId());
        assertThat(dto.getContent()).isEqualTo(foundDto.getContent());
    }
    
    // 같은 게시글에 두번 댓글을 다는 경우
    @Test
    void createDuplicate() {
        CommentCreateDTO dto1 = new CommentCreateDTO();
        dto1.setMemberId(1L);
        dto1.setBoardId(1L);
        dto1.setContent("첫번째 댓글입니다");

        CommentCreateDTO dto2 = new CommentCreateDTO();
        dto2.setMemberId(1L);
        dto2.setBoardId(1L);
        dto2.setContent("같은 게시글의 두번째 댓글입니다");

        Long commentId1 = commentService.create(dto1);
        Long commentId2 = commentService.create(dto2);

        CommentDTO foundDto1 = commentService.findOne(commentId1);
        CommentDTO foundDto2 = commentService.findOne(commentId2);

        assertThat(dto1.getMemberId()).isEqualTo(foundDto1.getMemberId());
        assertThat(dto1.getBoardId()).isEqualTo(foundDto1.getBoardId());
        assertThat(dto1.getContent()).isEqualTo(foundDto1.getContent());

        assertThat(dto2.getMemberId()).isEqualTo(foundDto2.getMemberId());
        assertThat(dto2.getBoardId()).isEqualTo(foundDto2.getBoardId());
        assertThat(dto2.getContent()).isEqualTo(foundDto2.getContent());
    }
    
    // 댓글 수정
    @Test
    void update() {
        CommentCreateDTO dto = new CommentCreateDTO();
        dto.setMemberId(1L);
        dto.setBoardId(1L);
        dto.setContent("첫번째 댓글입니다");

        Long commentId = commentService.create(dto);

        CommentUpdateDTO updateDTO = new CommentUpdateDTO();
        updateDTO.setContent("수정한 댓글입니다");

        Long updateId = commentService.update(commentId, updateDTO);

        CommentDTO foundDto = commentService.findOne(updateId);
        assertThat(foundDto.getContent()).isEqualTo(updateDTO.getContent());
    }
    
    // 댓글 삭제
    @Test
    void delete() {
        CommentCreateDTO dto = new CommentCreateDTO();
        dto.setMemberId(1L);
        dto.setBoardId(1L);
        dto.setContent("첫번째 댓글입니다");

        Long commentId = commentService.create(dto);

        commentService.delete(commentId);

        assertThatThrownBy(() -> commentService.findOne(commentId))
                .isInstanceOf(NoSuchElementException.class);
    }

create, update, delete메소드가 잘 동작하는지 테스트하기 위해 CreateDTO를 만들어 comment를 생성하고 UpdateDTO를 만들어 수정한다. findOne메소드를 통해 저장, 수정, 삭제된 comment를 확인하여 잘 작동하는지 검사한다.

참고로 비즈니스 요구사항에서 한 사용자가 동일한 게시글에 여러번 댓글을 다는 것에 대한 제한사항은 없기 때문에 creteDuplicate 메소드를 통해서 두 comment가 잘 저장되었는지 확인한다.

findOne

	// 없는 댓글을 찾는 경우
    @Test
    void findNothing() {
        assertThatThrownBy(() -> commentService.findOne(100L)).isInstanceOf(NoSuchElementException.class);
    }

findOne 메소드의 경우 위의 테스트를 통해 검증되었으므로 별도의 테스트 코드를 만들지 않았다. 대신 존재하지 않는 commentId를 조회하는 경우 NoSuchElementException을 던지도록 구현하였던 부분에 대한 테스트 코드를 작성하였다.

findByBoard

	// 게시글로 댓글 필터링
    @Test
    void findByBoard() {
        List<Member> members = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            Member member = Member.builder().name("testMember" + i).build();
            em.persist(member);
            members.add(member);
        }

        Board board = Board.builder().member(members.get(0)).content("testBoard").build();
        em.persist(board);

        List<Comment> comments = new ArrayList<>();
        for (Member member : members) {
            Comment comment = Comment.builder().board(board).member(member).content("testComment" + member.getId()).build();
            em.persist(comment);
            comments.add(comment);
        }

        List<CommentDTO> foundComments = commentService.findByBoard(board.getId()).getComments();
        assertThat(foundComments).extracting("boardId").containsOnly(board.getId());
        assertThat(foundComments).extracting("memberId")
                .containsExactlyElementsOf(comments.stream().map(comment -> comment.getMember().getId()).toList());
        assertThat(foundComments).extracting("commentId")
                .containsExactlyElementsOf(comments.stream().map(comment -> comment.getId()).toList());
        assertThat(foundComments).extracting("content")
                .containsExactlyElementsOf(comments.stream().map(comment -> comment.getContent()).toList());
    }

    // 게시글에 달린 댓글이 없는 경우
    @Test
    void findNothingByBoard() {
        Member member = Member.builder().name("testMember1").build();
        Board board = Board.builder().member(member).content("testBoard").build();
        em.persist(member);
        em.persist(board);

        List<CommentDTO> comments = commentService.findByBoard(board.getId()).getComments();

        assertThat(comments.size()).isEqualTo(0);
    }

    // 게시글이 없는 경우
    @Test
    void findByWrongBoardId() {
        Member member = Member.builder().name("testMember1").build();
        Board board = Board.builder().member(member).content("testBoard").build();
        em.persist(member);
        em.persist(board);

        assertThatThrownBy(() -> commentService.findByBoard(board.getId() + 100L))
                .isInstanceOf(NoSuchElementException.class);
    }

boardId로 comment를 필터링하여 조회할 때, 정상적인 경우라면 조회결과 나온 comment들의 boardId가 모두 같아야 하고, memberId, commentId, content들은 등록한 comment의 정보와 일치해야 한다. 따라서 첫번째 테스트에서 extracting메소드를 사용하여 각 필드를 리스트로 뽑고, containsOnly와 containsExactlyElementsOf메소드를 활용하여 각각의 필드값에 대해 검사하였다. 참고로 containsOnly는 리스트의 모든 원소들이 주어진 값만 포함하는지 검사하는 메소드이고, containsExactlyElementsOf는 순서에 상관없이 두 리스트의 요소를 검사하는 메소드이다.

다음으로 board에 달린 comment가 없는 경우 조회 결과로 나온 리스트의 길이가 0인지 검증하고, 만약 조회시 게시글이 없는 경우라면 NoSuchElementException을 던지는지 검사하였다.

findByMember

	// 작성자로 댓글 필터링
    @Test
    void findByMember() {
        List<Member> members = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            Member member = Member.builder().name("testMember" + i).build();
            em.persist(member);
            members.add(member);
        }
        Member member = members.get(0);

        List<Board> boards = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            Board board = Board.builder().member(members.get(i)).content("testBoard").build();
            em.persist(board);
            boards.add(board);
        }

        List<Comment> comments = new ArrayList<>();
        for (Board board : boards) {
            Comment comment = Comment.builder().board(board).member(member).content("testComment" + member.getId()).build();
            em.persist(comment);
            comments.add(comment);
        }

        List<CommentDTO> foundComments = commentService.findByMember(member.getId()).getComments();
        assertThat(foundComments).extracting("memberId").containsOnly(member.getId());
        assertThat(foundComments).extracting("boardId")
                .containsExactlyElementsOf(comments.stream().map(comment -> comment.getBoard().getId()).toList());
        assertThat(foundComments).extracting("commentId")
                .containsExactlyElementsOf(comments.stream().map(comment -> comment.getId()).toList());
        assertThat(foundComments).extracting("content")
                .containsExactlyElementsOf(comments.stream().map(comment -> comment.getContent()).toList());
    }

    // 사용자가 작성한 댓글이 없는 경우
    @Test
    void findNothingByMember() {
        Member member = Member.builder().name("testMember1").build();
        Board board = Board.builder().member(member).content("testBoard").build();
        em.persist(member);
        em.persist(board);

        List<CommentDTO> comments = commentService.findByMember(member.getId()).getComments();

        assertThat(comments.size()).isEqualTo(0);
    }

    // 사용자가 없는 경우
    @Test
    void findByWrongMemberId() {
        Member member = Member.builder().name("testMember1").build();
        Board board = Board.builder().member(member).content("testBoard").build();
        em.persist(member);
        em.persist(board);

        assertThatThrownBy(() -> commentService.findByMember(member.getId() + 100L))
                .isInstanceOf(NoSuchElementException.class);
    }

위의 테스트에서 boardId가 memberId로 바뀐 것을 제외하면 나머지 로직은 동일하다.

like

	// 새로운 댓글에 좋아요를 누르고, 다시 한번 눌러 좋아요를 취소한 경우
    @Test
    void like() {
        Member member = Member.builder().name("testMember1").build();
        Board board = Board.builder().member(member).content("testBoard").build();
        Comment comment = Comment.builder().member(member).board(board).content("testComment").build();
        em.persist(member);
        em.persist(board);
        em.persist(comment);

        em.flush();
        em.clear();

        commentService.like(comment.getId());

        CommentDTO like = commentService.findOne(comment.getId());
        assertThat(like.getLikeCount()).isEqualTo(1);

        commentService.like(comment.getId());

        CommentDTO unlike = commentService.findOne(comment.getId());
        assertThat(unlike.getLikeCount()).isEqualTo(0);
    }

좋아요 기능이 정상작동하는지 확인하기 위해 새로운 회원과 댓글을 만들고 like를 호출한다. 이후 댓글의 likeCount값이 1인지 검사하여 좋아요가 제대로 반영되었는지 검증한다. 또한 같은 사용자가 하나의 댓글에 좋아요를 두번 누른 경우 UNLIKE상태로 변하기 때문에 likeCount가 0인지 확인하여 정상적으로 좋아요 취소가 되었는지 검사한다.

문제점

  • 발생 가능한 예외 상황들을 좀 더 구체화하고 예외를 처리하는 로직을 추가해야 할 것 같다.

  • response가 체계적이지 않기 때문에 통일된 response형식을 만들고 상태코드를 통해 클라이언트에게 각 상황에 맞는 행동을 유도해야 할 것 같다.

  • 현재는 데이터가 없는 경우 NoSuchElementException을 던지기 때문에 클라이언트 입장에서는 500 Internal Server Error가 발생하는데 서비스나 컨트롤러계층에서 오류를 잡아서 404 Not Found로 바꿔주는 작업이 필요할 것 같다.

  • 지금은 POST나 PUT처럼 데이터를 바꾸는 요청에서 사용자의 정보를 requestDTO의 member_id필드로 받는데, 이런 방식은 클라이언트에서 member_id만 알면 다른 사용자의 정보를 변경하는 것이 가능해지기 때문에 보완이 필요할 것 같다. 추후 사용자 인증을 구현한 뒤 세션을 통해 사용자 정보를 얻어 요청을 처리하는 것이 더 안전할 것 같다.

  • DTO에 편의상 롬복의 @Data 어노테이션을 사용하였는데 실무에서 롬복을 사용할 때 주의해야 할 점이 있다고 들었다. 구체적인 내용들을 찾아보고 안전하다고 판단되는 곳에만 롬복을 사용해야 할 것 같다. 또한 @Builder에 대해서도 장단점을 찾아보고 필요한 곳에만 사용하는 게 좋을 것 같다.

2개의 댓글

comment-user-thumbnail
2023년 6월 16일

댓글을 저장할 수 bloxd io 있는 데이터베이스 모델을 설계했습니다. 댓글의 내용, 작성자, 작성일자 등을 저장할 필드를 포함했습니다.

답글 달기
comment-user-thumbnail
2024년 1월 15일

이러한 정보 보안은 우수한 효율성을 보장합니다. 이것은 정말 기대할 가치가 있습니다. 모든 정보에는 유용한 정보 블록을 제공하기 위한 특정 인코딩 프로세스가 있습니다. 효과적으로 사용하고 잘 발전하세요 geometry dash lite

답글 달기