본문 바로가기
미니 프로젝트/이노베이션 5주차

이노베이션 5주차 1 - 댓글, 연관관계(양방향), 도메인 내부에 비즈니스 로직 삽입

by 구너드 2023. 7. 7.

기존에 완성한 과제에서 댓글 기능과 권한 부여 기능을 추가적으로 작성하는 5주차 과제. 간단하게 erd를 그려보았다. 

 

기능 URL Method Request Header Response Header
회원 가입 /api/auth/signup POST    
로그인 /api/auth/login POST   Authorization:Bearer 
게시글 작성 /api/post POST Authorization:Bearer -  
게시글 목록 조회 /api/post GET    
게시글 상세 조회 /api/post/{id} GET    
게시글 수정 /api/post/{id} PATCH Authorization:Bearer -  
게시글 삭제 /api/post/{id} DELETE Authorization:Bearer -  
댓글 작성 /api/post/{id}/comment POST Authorization:Bearer -  
댓글 수정 /api/post/{id}/comment/{id} PATCH Authorization:Bearer -  
댓글 삭제 /api/post/{id}/comment/{id} DELETE Authorization:Bearer -  

추가적인 API


@Entity
@NoArgsConstructor
@Getter
public class Comment extends Timestamped {

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

    private String content;
    private String username;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "post_id")
    private Post post;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    public Comment(CommentRequestDto commentRequestDto, User user) {
        this.content = commentRequestDto.getContent();
        this.username = user.getUsername();
        this.user = user;
    }

    private void update(CommentRequestDto commentRequestDto) {
        this.content = commentRequestDto.getContent();
    }

    protected void setPost(Post post) {
        this.post = post;
    }
}

Comment 클래스

지연로딩을 위해 LAZY로 변경하고 연관관계 편의 메서드를 작성하는 게 편할 거 같아 protected 접근제어자를 이용하여 set을 설정했다.

 

@Entity
@Getter
@NoArgsConstructor
public class Post extends Timestamped {

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

    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    private String username;

    @Column(nullable = false)
    private String description;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Comment> commentList = new ArrayList<>();

    public void addComment(Comment comment) {
        commentList.add(comment);
        comment.setPost(this);
    }
}

Post 클래스. 양방향 연관관계에서 연관관계의 주인을 설정하는 이유는 어떤 클래스에서 외래키를 관리하며 해당 데이터에 대해 작업을 시행할지 결정하지 않는다면 딜레마에 빠질 수 있기 때문이다. 자세한 내용은 JPA 카테고리의 글 참조. 다만 여기서는 Post에 Comment가 추가되는 모습이 보다 객체들의 관계를 잘 설명해준다고 생각했기 때문에 연관관계의 주인은 외래키를 관리하는 Comment로 설정했지만 연관관계 편의 메서드는 Post 클래스에 작성하였다.


@RestController
@RequestMapping("/api/post/{postId}/comment")
@RequiredArgsConstructor
public class CommentController {

    private final CommentService commentService;

    @PostMapping
    public CommentResponseDto createComment(@PathVariable Long postId,
                                            @RequestBody CommentRequestDto commentRequestDto,
                                            @AuthenticationPrincipal UserDetailsImpl userDetailsImpl) {
        return commentService.createComment(postId, commentRequestDto, userDetailsImpl.getUser());
    }

    @PatchMapping("/{commentId}")
    public CommentResponseDto modifyComment(@PathVariable Long commentId,
                                            @RequestBody CommentRequestDto commentRequestDto,
                                            @AuthenticationPrincipal UserDetailsImpl userDetailsImpl) {
        return commentService.updateComment(commentId, commentRequestDto, userDetailsImpl.getUser());
    }

    @DeleteMapping ("/{commentId}")
    public String removeComment(@PathVariable Long commentId,
                                @AuthenticationPrincipal UserDetailsImpl userDetailsImpl) {
        return commentService.deleteComment(commentId, userDetailsImpl.getUser());
    }
}

어떤 Post에 Comment가 작성되었는지 종속성을 표시해주기 위해 Url에 postid 가 표시되도록 하였다. Post클래스의 controller와 크게 다르지 않는 모습을 볼 수 있다.

 

@Service
@Transactional
@RequiredArgsConstructor
public class CommentService {

    private final CommentRepository commentRepository;
    private final PostService postService;

    public CommentResponseDto createComment(Long postId, CommentRequestDto commentRequestDto, User user) {
        Comment comment = new Comment(commentRequestDto, user);
        postService.findPost(postId).addComment(comment);
        commentRepository.save(comment);
        return new CommentResponseDto(comment);
    }

    public CommentResponseDto updateComment(Long commentId, CommentRequestDto commentRequestDto, User user) {
        Comment comment = findComment(commentId).changeComment(commentRequestDto, user);
        return new CommentResponseDto(comment);
    }

    public String deleteComment(Long commentId, User user) {
        Comment comment = findComment(commentId).checkDeleteableComment(user);
        commentRepository.delete(comment);
        return "삭제완료";
    }

    @Transactional(readOnly = true)
    public Comment findComment(Long commentId) {
        return commentRepository.findById(commentId).orElseThrow(()->
                new IllegalArgumentException("해당 댓글은 존재하지 않습니다"));
    }
}

연관관계 편의 메서드를 이용하여 Post에 Comment가 추가되면 해당 Comment에도 Post를 추가해준다. 종속성을 위해 Url에 기재된 postId를 바탕으로 해당 Post를 찾고 각 API가 요구하는 기능들을 구현했다. 여기서 update와 delete는 보다 도메인적인 요소가 강하다고 생각하고 있었는데 이를 도메인 내부의 메서드로 바꿔보는 게 어떨까라는 조언을 받고 해당 비즈니스 로직들을 도메인으로 넘겼다.

 

// Post

    private void update(PostRequestDto postRequestDto) {
        this.title = postRequestDto.getTitle();
        this.description = postRequestDto.getDescription();
    }

    public Post changePost(PostRequestDto postRequestDto, User user) {
        if (user.getId() != this.getUser().getId() && user.getRole().getAuthority() == "ROLE_USER") throw new IllegalArgumentException("해당 게시글 작성자 혹은 관리자만 수정할 수 있습니다");
        this.update(postRequestDto);
        return this;
    }

    public Post checkDeleteablePost(User user) {
        if (user.getId() != this.getUser().getId() && user.getRole().getAuthority() == "ROLE_USER") throw new IllegalArgumentException("해당 게시글 작성자 혹은 관리자만 삭제할 수 있습니다");
        return this;
    }
    
    
    
// Comment

    private void update(CommentRequestDto commentRequestDto) {
        this.content = commentRequestDto.getContent();
    }

    public Comment changeComment(CommentRequestDto commentRequestDto, User user) {
        if (user.getId() != this.getUser().getId() && user.getRole().getAuthority() == "ROLE_USER") throw new IllegalArgumentException("해당 댓글 작성자 혹은 관리자만 수정할 수 있습니다");
        this.update(commentRequestDto);
        return this;
    }

    public Comment checkDeleteableComment(User user) {
        if (user.getId() != this.getUser().getId() && user.getRole().getAuthority() == "ROLE_USER") throw new IllegalArgumentException("해당 댓글 작성자 혹은 관리자만 삭제할 수 있습니다");
        return this;
    }

해당 비즈니스 로직을 도메인에 삽입함으로써 도메인의 상태를 변경하는 void update메서드를 private으로 설정하여 외부의 접근에 의해 변경될 수 있는 변경 가능성이 사라지고 change와 checkdeleteable 메서드를 통해서만 해당 조건에 의해 도메인의 상태가 변경될 수 있게끔 설정할 수 있었다. 자신이 송신받는 메세지를 통해 상태를 바꿀 수 있는 이러한 설계는 보다 객체지향적인 설계라고 생각이 들었다.


이 후 포스트맨을 이용하여 테스트를 해보는데 밑의 그림과 같이 comment의 외래키인 user_id가 null로 들어가 있는 점을 확인할 수 있었다. 때문에 해당 댓글은 작성은 가능하나, 수정, 삭제가 불가능한 상태임을 확인할 수 있었다. 이 부분의 해결방법을 찾던 중 Comment 클래스의 생성자에 User user를 넣지 않은 사소한 실수를 확인할 수 있었다.

    public Comment(CommentRequestDto commentRequestDto, User user) {
        this.content = commentRequestDto.getContent();
        this.username = user.getUsername();
        this.user = user;
    }

해당 오류를 수정하고 테스트를 해본 결과 원하는 방식대로 작성,수정,삭제가 가능해졌다. 일단 오늘은 여기까지