이노베이션 5주차 1 - 댓글, 연관관계(양방향), 도메인 내부에 비즈니스 로직 삽입
기존에 완성한 과제에서 댓글 기능과 권한 부여 기능을 추가적으로 작성하는 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;
}
해당 오류를 수정하고 테스트를 해본 결과 원하는 방식대로 작성,수정,삭제가 가능해졌다. 일단 오늘은 여기까지