과거 댓글기능을 넣는 과정에서, 댓글-및-대댓글-구현기 와 같이 구현을 했었다. 이 당시 서로 다른 게시판에서 댓글에 대해 생성, 조회, 삭제 해야했기 때문에 생성 및 조회하는 댓글이 어떤 게시판에 해당하는 댓글인지 확인을 해야했다. 그 해결법으로 무지성 if 문을 통해 각 게시판에 대한 분기를 처리하면서 코드가 지저분해졌다. 그래서 리팩토링 해보았다.
이 내용은
크게 두가지 문제가 있었는데
package mokindang.jubging.project_backend.comment.service;
@Service
@RequiredArgsConstructor
public class CommentService {
//...
@Transactional
public BoardIdResponse addComment(final Long memberId, final BoardType boardType, final Long boardId,
final CommentCreationRequest commentCreationRequest) {
Member writer = memberService.findByMemberId(memberId);
LocalDateTime now = LocalDateTime.now();
**if (boardType == BoardType.RECRUITMENT_BOARD)** {
//구인 게시글 조회
RecruitmentBoard board = recruitmentBoardService.findByIdWithOptimisticLock(boardId);
// 댓글을 생성하고 구인 게시글과 연관관계를 맺는 메서드 호출
Comment comment = Comment.createOnRecruitmentBoardWith(board, commentCreationRequest.getCommentBody(), writer, now);
commentRepository.save(comment);
return new BoardIdResponse(boardId);
}
**if (boardType == BoardType.CERTIFICATION_BOARD)** {
//인증 게시글 조회
CertificationBoard board = certificationBoardService.findById(boardId);
//댓글을 생성하고 인증 게시글과 연관관계를 맺는 메서드 호출
Comment comment = Comment.createOnCertificationBoardWith(board, commentCreationRequest.getCommentBody(), writer, now);
commentRepository.save(comment);
return new BoardIdResponse(boardId);
}
throw new IllegalArgumentException("존재 하지 않는 게시판에 대한 접근입니다.");
}
//...
}
package mokindang.jubging.project_backend.comment.service;
@Service
@RequiredArgsConstructor
public class CommentService {
//...
@Transactional
public MultiCommentSelectionResponse selectComments(final Long memberId, final BoardType boardType, final Long boardId) {
if (boardType == BoardType.RECRUITMENT_BOARD) {
List<Comment> commentsByRecruitmentBoard = commentRepository.findCommentsByRecruitmentBoardId(boardId);
Member member = memberService.findByMemberId(memberId);
RecruitmentBoard board = recruitmentBoardService.findByIdWithOptimisticLock(boardId);
//회원에 대해 게시판에 있는 댓글에 대한 검증 로직이 이루어짐
boolean writingCommentPermission = board.isSameRegion(member.getRegion());
boolean isWriterParticipatedIn = board.isParticipatedIn(memberId);
return new MultiCommentSelectionResponse(convertToRecruitmentBoardCommentSelectionResponse(memberId, commentsByRecruitmentBoard, board, isWriterParticipatedIn),
writingCommentPermission);
}
if (boardType == BoardType.CERTIFICATION_BOARD) { //검증 없이 조회 가능
List<Comment> commentsByCertificationBoard = commentRepository.findCommentsByCertificationBoardId(boardId);
CertificationBoard board = certificationBoardService.findById(boardId);
return new MultiCommentSelectionResponse(convertToCertificationBoardCommentSelectionResponse2(memberId, commentsByCertificationBoard, board),
true);
}
throw new IllegalArgumentException("존재 하지 않는 게시판에 대한 접근입니다.");
}
private List<CommentSelectionResponse> convertToRecruitmentBoardCommentSelectionResponse(final Long memberId, final List<Comment> commentsByRecruitmentBoard, final RecruitmentBoard board, final boolean isWriterParticipatedIn) {
return commentsByRecruitmentBoard.stream()
.map(comment -> new CommentSelectionResponse(comment, memberId, board.isSameWriterId(comment.getWriter().getId()), isWriterParticipatedIn))
.collect(Collectors.toUnmodifiableList());
}
private List<CommentSelectionResponse> convertToCertificationBoardCommentSelectionResponse2(final Long memberId, final List<Comment> commentsByRecruitmentBoard, final CertificationBoard board) {
return commentsByRecruitmentBoard.stream()
.map(comment -> new CommentSelectionResponse(comment, memberId, board.isSameWriterId(comment.getWriter().getId()), false))
.collect(Collectors.toUnmodifiableList());
}
//...
}
addComment( )
를 보면 컨트롤러에서 받은 boardType 에 따라 해당하는 게시글에 댓글을 추가한다.
댓글을 생성 할 때 마다 게시판과의 연관관계를 이어주게 하기 위해 각 게시판마다 댓글 생성이 가능한 정적 팩토리메서드를 호출함 하는 과정에서 서비스 레이어 에서의 코드가 복잡하게 되었음.
이런 문제는 결국 jpa 를 사용하면서 발생하는 연관관계를 이어주는 과정에서 발생한 문제라고 생각함. 따라서 Comment 엔티티 구조 자체가 문제가 아닐까?
라고 생각함.
recruitmentBoard
와 certificationBoard
에서 댓글을 사용함.recruitmentBoard
와 certificationBoard
외에 댓글 기능을 사용하는 새로운 요구사항이 추가될 때 Comment 에 필드를 추가하는 것은 유연하지 않다고 생각함.@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "comment_id", nullable = false)
Long id;
//...
**@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "recruitment_board_id")
private RecruitmentBoard recruitmentBoard;**
**@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "certification_board_id")
private CertificationBoard certificationBoard;**
//...
public static Comment **createOnRecruitmentBoardWith**(final RecruitmentBoard board, final String commentBody, final Member writer, final LocalDateTime now) {
return new Comment(board, commentBody, writer, now);
}
private Comment(final RecruitmentBoard board, final String commentBody, final Member writer, final LocalDateTime now) {
this.commentBody = new CommentBody(commentBody);
this.writer = writer;
this.createdDateTime = now;
this.lastModifiedDateTime = createdDateTime;
setRecruitmentBoard(board);
}
private void setRecruitmentBoard(final RecruitmentBoard recruitmentBoard) {
this.recruitmentBoard = recruitmentBoard;
recruitmentBoard.addComment(this);
}
public static Comment **createOnCertificationBoardWith**(final CertificationBoard board, final String commentBody, final Member writer, final LocalDateTime now) {
return new Comment(board, commentBody, writer, now);
}
private Comment(final CertificationBoard board, final String commentBody, final Member writer, final LocalDateTime now) {
this.commentBody = new CommentBody(commentBody);
this.writer = writer;
this.createdDateTime = now;
this.lastModifiedDateTime = createdDateTime;
setCertificationBoard(board);
}
private void setCertificationBoard(final CertificationBoard certificationBoard) {
this.certificationBoard = certificationBoard;
certificationBoard.addComment(this);
}
//...
}
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "comment_id", nullable = false)
private Long id;
@Column
private Long boardId;
@Enumerated(EnumType.STRING)
private BoardType boardType;
public Comment(final Long boardId, final BoardType boardType, final String commentBody, final Member writer, final LocalDateTime now) {
this.boardId = boardId;
this.boardType = boardType;
this.commentBody = new CommentBody(commentBody);
this.writer = writer;
this.createdDateTime = now;
this.lastModifiedDateTime = createdDateTime;
}
//...
}
Comment 리팩토링 후 CommentService 의 addComment 메서드
@Transactional
public BoardIdResponse addComment(final Long memberId, final BoardType boardType, final Long boardId,
final CommentCreationRequest commentCreationRequest) {
Member writer = memberService.findByMemberId(memberId);
LocalDateTime now = LocalDateTime.now();
Comment comment = new Comment(boardId, boardType, commentCreationRequest.getCommentBody(), writer, now);
commentRepository.save(comment);
return new BoardIdResponse(comment.getBoardId());
}
}
각 게시판 마다 따라 댓글 반환하는 전략이 다르다 때문에 전략패턴으로 해결을 해보려고한다.
따라서 이를 처리하는 전략들을 만들어두고, BoardType 에 따라서 그에 맞는 댓글 반환 전략을 통해 댓글을 반환하면 될 것 같다.
package mokindang.jubging.project_backend.comment.service;
@Service
@RequiredArgsConstructor
public class CommentService {
//...
@Transactional
public MultiCommentSelectionResponse selectComments(final Long memberId, final BoardType boardType, final Long boardId) {
if (boardType == BoardType.RECRUITMENT_BOARD) {
List<Comment> commentsByRecruitmentBoard = commentRepository.findCommentsByRecruitmentBoardId(boardId);
Member member = memberService.findByMemberId(memberId);
RecruitmentBoard board = recruitmentBoardService.findByIdWithOptimisticLock(boardId);
//회원에 대해 게시판에 있는 댓글에 대한 검증 로직이 이루어짐
boolean writingCommentPermission = board.isSameRegion(member.getRegion());
boolean isWriterParticipatedIn = board.isParticipatedIn(memberId);
return new MultiCommentSelectionResponse(convertToRecruitmentBoardCommentSelectionResponse(memberId, commentsByRecruitmentBoard, board, isWriterParticipatedIn),
writingCommentPermission);
}
if (boardType == BoardType.CERTIFICATION_BOARD) { //검증 없이 조회 가능
List<Comment> commentsByCertificationBoard = commentRepository.findCommentsByCertificationBoardId(boardId);
CertificationBoard board = certificationBoardService.findById(boardId);
return new MultiCommentSelectionResponse(convertToCertificationBoardCommentSelectionResponse2(memberId, commentsByCertificationBoard, board),
true);
}
throw new IllegalArgumentException("존재 하지 않는 게시판에 대한 접근입니다.");
}
private List<CommentSelectionResponse> convertToRecruitmentBoardCommentSelectionResponse(final Long memberId, final List<Comment> commentsByRecruitmentBoard, final RecruitmentBoard board, final boolean isWriterParticipatedIn) {
return commentsByRecruitmentBoard.stream()
.map(comment -> new CommentSelectionResponse(comment, memberId, board.isSameWriterId(comment.getWriter().getId()), isWriterParticipatedIn))
.collect(Collectors.toUnmodifiableList());
}
private List<CommentSelectionResponse> convertToCertificationBoardCommentSelectionResponse2(final Long memberId, final List<Comment> commentsByRecruitmentBoard, final CertificationBoard board) {
return commentsByRecruitmentBoard.stream()
.map(comment -> new CommentSelectionResponse(comment, memberId, board.isSameWriterId(comment.getWriter().getId()), false))
.collect(Collectors.toUnmodifiableList());
}
//...
}
CommentsSelectionStrategy
) 생성CommentsSelectionStrategy
를 반환하는 Finder 를 통해 CommentService 에서 CommentsSelectionStrategy
를 반환 하도록함.CommentsSelectionStrategy
public interface CommentsSelectionStrategy {
MultiCommentSelectionResponse selectComments(final Long boardId, final Long memberId);
BoardType getBoardType();
}
RecruitmentBoardCommentsSelectionStrategy - 구인 게시글
@Component
@RequiredArgsConstructor
public class RecruitmentBoardCommentsSelectionStrategy implements CommentsSelectionStrategy {
private static final BoardType BOARD_TYPE = BoardType.RECRUITMENT_BOARD;
private final CommentRepository commentRepository;
private final MemberService memberService;
private final RecruitmentBoardService recruitmentBoardService;
@Override
public MultiCommentSelectionResponse selectComments(Long boardId, Long memberId) {
List<Comment> commentsByRecruitmentBoard = commentRepository.findCommentByBoardTypeAndBoardId(BOARD_TYPE, boardId);
Member member = memberService.findByMemberId(memberId);
RecruitmentBoard board = recruitmentBoardService.findByIdWithOptimisticLock(boardId);
boolean writingCommentPermission = board.isSameRegion(member.getRegion());
boolean isWriterParticipatedIn = board.isParticipatedIn(memberId);
return new MultiCommentSelectionResponse(convertToRecruitmentBoardCommentSelectionResponse(memberId, commentsByRecruitmentBoard, board, isWriterParticipatedIn), writingCommentPermission);
}
//...
@Override
public BoardType getBoardType() {
return BOARD_TYPE;
}
}
CertificationBoardCommentsSelectionStrategy - 인증 게시글
@Component
@RequiredArgsConstructor
public class CertificationBoardCommentsSelectionStrategy implements CommentsSelectionStrategy {
private static final BoardType BOARD_TYPE = BoardType.CERTIFICATION_BOARD;
private final CommentRepository commentRepository;
private final CertificationBoardService certificationBoardService;
@Override
public MultiCommentSelectionResponse selectComments(Long boardId, Long memberId) {
List<Comment> commentsByCertificationBoard = commentRepository.findCommentByBoardTypeAndBoardId(BOARD_TYPE, boardId);
CertificationBoard board = certificationBoardService.findById(boardId);
return new MultiCommentSelectionResponse(convertToCertificationBoardCommentSelectionResponse2(memberId, commentsByCertificationBoard, board),
true);
}
//...
@Override
public BoardType getBoardType() {
return BOARD_TYPE;
}
}
CommentSelectionStrategyFinder
@Component
@RequiredArgsConstructor
public class CommentsSelectionStrategyFinder {
private final Set<CommentsSelectionStrategy> commentsSelectionStrategies;
public CommentsSelectionStrategy getCommentSelectionStrategy(final BoardType boardType) {
return commentsSelectionStrategies.stream()
.filter(strategy -> strategy.getBoardType() == boardType)
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 boardType 입니다."));
}
}
@Service
@RequiredArgsConstructor
public class CommentService {
//...
private final CommentsSelectionStrategyFinder commentsSelectionStrategyFinder;
@Transactional
public MultiCommentSelectionResponse selectComments(final Long memberId, final BoardType boardType, final Long boardId){
CommentsSelectionStrategy commentSelectionStrategy = commentsSelectionStrategyFinder.getCommentSelectionStrategy(boardType);
return commentSelectionStrategy.selectComments(boardId, memberId);
}
//...
}
CascadeType
설정으로 관리 했음.1번 문제 - Comment 및 addComment() 메서드 리팩토링
2번 문제 - 전략패턴을 통한 selectCommnets() 리팩토링
Strategy
에서 처리하면서 로직을 캡슐화 할 수 있었다. 결과적으로 CommentService
내부 코드의 복잡도를 낮출 수 있었다.https://github.com/mokInDang/project_backend/pull/412
끝.
BoardType 네이밍 CommentType