전체 코드는 여기에서 볼 수 있습니다.

오늘은 게시물에 댓글 기능을 적용해보도록 하겠습니다.

- Controller, Service, Entity, Repository, DTO

- CommentController

@RestController
@RequiredArgsConstructor
public class CommentController {

    private final CommentService commentService;

    @PostMapping("/post/{id}/comment")
    @ResponseStatus(value = HttpStatus.OK)
    @PreAuthorize("isAuthenticated()")
    @ApiOperation(value = "댓글 쓰기", notes = "id에 해당하는 게시글에 댓글을 작성합니다.")
    public SuccessResponse<String> createComment(@PathVariable(name = "id") Long postId,
                                                 @Valid @RequestBody CommentDTO commentDTO,
                                                 Principal principal) {

        CommentDTO comment = commentService.createComment(postId, commentDTO, principal.getName());

        return SuccessResponse.success(null);

    }
}

- CommentService

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CommentService {

    private final CommentRepository commentRepository;
    private final PostRepository postRepository;
    private final UserRepository userRepository;

    @Transactional
    public CommentDTO createComment(Long postId, CommentDTO commentDTO, String email) {
        Optional<Post> byId = postRepository.findById(postId);

        Post post = byId.orElseThrow(() -> new PostNotFound("게시물이 삭제되었거나 존재하지 않습니다."));

        Optional<User> byEmail = userRepository.findByEmail(email);

        User user = byEmail.orElseThrow(() -> new UsernameNotFoundException("유저를 찾을 수 없습니다."));

        Comment comment = Comment.builder()
                .contents(commentDTO.getContents())
                .build();

        comment.mappingPostAndUser(post, user);

        Comment saveComment = commentRepository.save(comment);

        return CommentDTO.convertToCommentDto(saveComment);
    }
}

- CommentEntity

@Entity
@Getter
@Builder
@AllArgsConstructor(access = PROTECTED)
@NoArgsConstructor(access = PROTECTED)
public class Comment {

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

    @Column(nullable = false, length = 100)
    private String contents;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "post_id", foreignKey = @ForeignKey(name = "FK_user_comment"))
    private Post post;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "user_id", foreignKey = @ForeignKey(name = "FK_post_comment"))
    private User user;

    public void mappingPostAndUser(Post post, User user) {
        this.post = post;
        this.user = user;

        post.mappingComment(this);
        user.mappingComment(this);
    }
}

- CommentRepository

@Repository
public interface CommentRepository extends JpaRepository<Comment, Long> {
}

- CommentDTO

@Builder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
@Data
public class CommentDTO {
    private Long id;

    @NotBlank
    @Length(max = 100)
    private String contents;
    private UserDTO user;

    public static CommentDTO convertToCommentDto(Comment comment) {
        return CommentDTO.builder()
                .id(comment.getId())
                .contents(comment.getContents())
                .user(UserDTO.convertToUserDTO(comment.getUser()))
                .build();
    }

    public static List<CommentDTO> convertToCommentDtoList(List<Comment> commentList) {
        Stream<Comment> stream = commentList.stream();

        return stream.map(CommentDTO::convertToCommentDto).collect(Collectors.toList());

    }
}

 

여기 까지가 Comment 관련 코드이고 이제 Comment와 Post, User와 관계를 지정해주어야 합니다.

Comment와 Post는 N : 1 관계, Comment와 User는 N : 1 관계 입니다. 그러므로 PostEntity와 UserEntity에 연관관계및 연관관계 메서드를 설정해줍니다.

- PostEntity

    @OneToMany(fetch = LAZY, mappedBy = "post")
    private List<Comment> commentList = new ArrayList<>();
    
        public void mappingComment(Comment comment) {
        this.commentList.add(comment);
    }

- UserEntity

@OneToMany(fetch = FetchType.LAZY, mappedBy = "user")
    private List<Comment> commentList = new ArrayList<>();
    
    public void mappingComment(Comment comment) {
        commentList.add(comment);
    }

 

그리고 이제 게시물에 댓글을 달고 게시물을 조회하고 나면 쿼리가 무언가 이상합니다. 

1번 게시물에 1번 유저, 2번 유저, 3번 유저가 댓글을 달경우 1번 게시물을 조회할때 1, 2, 3번 유저를 조회하는 쿼리가 나가기 때문입니다.

이를 해결하기 위해 저번에 작업한 PostRepository에 최적화 작업을 하겠습니다.

- PostRepository

@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
    @EntityGraph(attributePaths = {"category", "user", "commentList.user"})
    @Override
    Optional<Post> findById(Long postId);

    @EntityGraph(attributePaths = {"user", "category"})
    @Override
    List<Post> findAll(Sort sort);
}

@EntityGraph에 "commentList.user"를 추가해줌으로써 CommentList와 여기에 댓글을 작성한 유저의 정보까지 한번에 가져오므로 쿼리 1번에 모두 조회가 가능합니다.

전체 코드는 여기에서 보실 수 있습니다.

먼저 코드를 보여드리도록 하겠습니다.

Controller, Service, PostEntity

- PostController

@PostMapping("/post")
    @ResponseStatus(value = HttpStatus.CREATED)
    @PreAuthorize("isAuthenticated()")
    @ApiOperation(value = "게시물 생성", notes = "게시물을 생성합니다.")
    public SuccessResponse<PostDTO> createPost(@Valid @RequestBody PostDTO postDTO, Principal principal) {
        log.info(principal.getName());
        PostDTO post = postService.createPost(postDTO, principal.getName());

        return SuccessResponse.success(post);
    }

 

이전 Json으로 로그인하기를 했을때 UserController의 login 메서드에서 email, password를 받아 UsernamePasswordAuthenticationToken을 만들어주고 이것을 UserAuthenticationProvider에 넘겨주었습니다. Provider에서 넘겨준 토큰으로 email과 password를 꺼내 DB에서 유저 정보를 조회해 로그인이 성공하면 UsernamePasswordAuthenticationToken으로 Authentication을 리턴하였습니다. 그로인해 로그인이 Principal를 통해 현재 로그인한 유저의 정보를 가져올 수 있습니다.

- PostService

public PostDTO createPost(PostDTO postDTO, String email) {
        Optional<User> byEmail = userRepository.findByEmail(email);

        User user = byEmail.orElseThrow(() -> new UsernameNotFoundException("게시글 작성 권한이 없습니다."));

        Post post = Post.builder()
                .title(postDTO.getTitle())
                .contents(postDTO.getContents())
                .createAt(LocalDateTime.now())
                .build();

        post.mappingCategory(postCategoryRepository.findByName(postDTO.getCategory()));
        post.mappingUser(user);

        Post savePost = postRepository.save(post);

        return PostDTO.builder()
                .id(savePost.getId())
                .build();
    }

받은 email로 DB에서 유저를 찾은 다음에 post에 매핑시켜 줍니다.

- UserEntity, PostEntity

- UserEntity

@Entity
@Builder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class User implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String email;

    private String password;

    private String nickname;

    private LocalDateTime createdBy;

    @Builder.Default
    private String role = "ROLE_USER";

    private String authority;

    private boolean enabled = true;

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "user")
    private List<Post> postList = new ArrayList<>();

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    public void encryptPassword(String password) {
        this.password = BCrypt.hashpw(password, BCrypt.gensalt());
    }

    public void mappingPost(Post post) {
        postList.add(post);
    }


    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Set<GrantedAuthority> auth = new HashSet<>();
        auth.add(new SimpleGrantedAuthority(authority));

        return auth;
    }

    @Override
    public String getUsername() {
        return this.getEmail();
    }

    @Override
    public boolean isAccountNonExpired() {
        return false;
    }

    @Override
    public boolean isAccountNonLocked() {
        return false;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return false;
    }

    @Override
    public boolean isEnabled() {
        return this.enabled;
    }


}

- PostEntity

@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    private String contents;

    private LocalDateTime createAt;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "post_category_id")
    private PostCategory category;

    @Builder.Default
    private Long viewCount = 0L;

    public void addViewCount() {
        this.viewCount += 1;
    }

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

////////////////////////////////////////////////////////////////////////////////////////////////

    public void changeTitle(String title) {
        this.title = title;
    }

    public void changeContents(String contents) {
        this.contents = contents;
    }

    public void mappingCategory(PostCategory postCategory) {
        this.category = postCategory;
        postCategory.mappingPost(this);
    }

    public void mappingUser(User user) {
        this.user = user;
        user.mappingPost(this);
    }
}

유저는 포스트를 여러개 작성 할수 있고 포스트는 유저 1명에게 속해 있어야하니 유저 : 포스트 는 1 : N 관계 입니다. 그러므로 User Entity에는 @ManyToOne 으로 매핑 시켜 주고, 포스트에는 @OneToMany로 매핑 시켜줍니다. N인 Post가 매핑관계의 주인이 되는게 좋으므로 mappedBy를 통해 Post의 user 필드를 지정해 주고 Post Entity에 유저를 매핑시켜줄 연관관계 메서드를 작성해줍니다.

여기 까지 진행한 후 게시물을 만들고 getPost를 통해 게시물을 조회하게 되면 쿼리가 2개가 발생하게 됩니다. Post를 가져올때 한번, Post에 매핑된 User를 가져올때 한번 일어나게 되는데요 이것을 쿼리 한번에 조회하기 위해 PostRepository를 수정해줍니다.

PostRepository

@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
    @EntityGraph(attributePaths = {"category", "user"})
    Optional<Post> findById(Long postId);
}

지난시간 postCategory 쿼리를 최적화 하기위해 @EntityGraph에 category를 지정해줬는데 user를 추가시켜주어 쿼리 1번에 유저까지 가져올수 있도록 합니다.

그 후 게시물 조회를 해보면 유저까지 한번에 가져오는걸 알 수 있습니다.

+ Recent posts