질문, 피드백 등 모든 댓글 환영합니다.
Controller에 이어 Service 계층을 개발하겠습니다.
개발 순서는 PostService (PostRepository) -> HeartService -> CommentService -> MemberService 입니다.
기본적으로 Service에서 DTO 를 생성하고 반환하여 Controller 가 Entity를 의존하지 않도록 개발했습니다. 또한 잘못된 접근을 시도한다면 (조회한 엔티티가 null) 예외를 발생시켰습니다.
PostService
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class PostService {
private final PostRepository postRepository;
private final MemberRepository memberRepository;
public List<PostListDto> findList() {
List<Post> find = postRepository.findPostList();
return find.stream().map(post -> {
return new PostListDto(post.getId(), post.getTitle(), post.getMember().getName(),
post.getHeartNum(), post.getCommentNum(), post.getCreatedDate());
}).collect(Collectors.toList());
}
public PostDto findPostAndComment(Long post_id) {
Post findPost = postRepository.findById(post_id).orElseThrow(IllegalArgumentException::new);
List<CommentDto> commentDtos = findPost.getComments().stream().map(comment -> {
return new CommentDto(comment.getId(), comment.getBody(), comment.getMember().getId(), comment.getMember().getName());
}).collect(Collectors.toList());
return new PostDto(findPost.getId(), findPost.getTitle(), findPost.getBody(), findPost.getMember().getId(), findPost.getMember().getName(), findPost.getHeartNum(), findPost.getCreatedDate(), commentDtos);
}
public WritePostDto findWritePostDto(Long post_id) {
Post findPost = postRepository.findById(post_id).orElseThrow(IllegalArgumentException::new);
return new WritePostDto(findPost.getId(), findPost.getTitle(), findPost.getBody());
}
}
기본적인 조회 로직은 Repository에서 엔티티 조회 후 DTO 스펙에 맞게 변환합니다.
public class PostService {
@Transactional
public Long createPost(WritePostDto writePostDto, Long id) {
Member findMember = memberRepository.findById(id).orElseThrow(IllegalArgumentException::new);
Post post = Post.createPost(writePostDto.getTitle(), writePostDto.getBody(), findMember);
return postRepository.save(post).getId();
}
@Transactional
public void updatePost(Long post_id, WritePostDto writePostDto) {
Post post = postRepository.findById(post_id).orElseThrow(IllegalArgumentException::new);
post.update(writePostDto.getTitle(), writePostDto.getBody());
}
@Transactional
public void delete(Long post_id) {
postRepository.deleteById(post_id);
}
}
updatePost() 는 게시글의 수정을 담당하는 로직으로 Post 엔티티를 조회하여 JPA의 변경 감지(더티 체킹) 기능으로 수정했습니다.
PostRepository
public interface PostRepository extends JpaRepository<Post, Long> {
@Query("select p from Post p left join fetch p.member")
List<Post> findPostList();
}
findPostList()
에서 JPQL을 이용하여 Post를 조회하며 연관된 Member를 페치 조인하였습니다.
PostDto
@Getter @Setter
@AllArgsConstructor
public class PostDto {
private Long id;
private String title;
private String body;
private Long member_id;
private String membername;
private Integer heartNum;
private LocalDateTime createdDate;
private List<CommentDto> commentDtos;
}
PostListDto
@Getter @Setter
@AllArgsConstructor
public class PostListDto {
private Long id;
private String title;
private String membername;
private int HeartNum;
private int commentNum;
private LocalDateTime createdDate;
}
WritePostDto
@Getter
@Setter
@AllArgsConstructor
public class WritePostDto {
private Long id;
private String title;
private String body;
}
HeartService
@Service
@RequiredArgsConstructor
public class HeartService {
private final PostRepository postRepository;
private final MemberRepository memberRepository;
private final HeartRepository heartRepository;
@Transactional
public void changeHeartStatus(Long post_id, Long member_id) {
heartRepository.findByMemberIdAndPostId(post_id, member_id).ifPresentOrElse(
heart -> {
heartRepository.delete(heart);
heart.getPost().minusHeartNum();
},
() -> {
Post post = postRepository.findById(post_id).orElseThrow(IllegalArgumentException::new);
Member member = memberRepository.findById(member_id).orElseThrow(IllegalArgumentException::new);
heartRepository.save(Heart.createHeart(post, member));
post.plusHeartNum();
});
}
}
게시글에 좋아요를 남기는 기능은 이 메서드 하나로 처리합니다.
기존에 '좋아요' 요청을 보낸 이력이 있는 경우 (조회 결과가 존재) Heart 테이블을 삭제하고 게시글 좋아요 개수를 -1 시킵니다.
반대의 경우 Heart 테이블을 생성하고 게시글 좋아요 개수를 +1 시킵니다.
HeartRepository
public interface HeartRepository extends JpaRepository<Heart, Long> {
@Query("select h from Heart h where h.post.id = :post_id and h.member.id = :member_id")
Optional<Heart> findByMemberIdAndPostId(@Param("post_id") Long post_id, @Param("member_id") Long member_id);
}
스프링 데이터 JPA는 엔티티를 외래키로 조회 시 Join 하여 조회하므로 JPQL로 직접 작성하였습니다. 해당 부분은 이전에 작성한 블로그 참고 바랍니다.
CommentService
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class CommentService {
private final CommentRepository commentRepository;
private final PostRepository postRepository;
private final MemberRepository memberRepository;
public EditCommentDto findCommentDto(Long comment_id) {
Comment comment = commentRepository.findById(comment_id).orElseThrow(IllegalArgumentException::new);
return new EditCommentDto(comment.getId(), comment.getBody());
}
@Transactional
public void save(CommentDto commentDto, Long post_id, Long member_id) {
Post post = postRepository.findById(post_id).orElseThrow(IllegalArgumentException::new);
Member member = memberRepository.findById(member_id).orElseThrow(IllegalArgumentException::new);
Comment comment = Comment.createComment(commentDto.getBody(), member, post);
commentRepository.save(comment);
post.plusCommentNum();
}
@Transactional
public void update(Long comment_id, CommentDto commentDto) {
Comment comment = commentRepository.findById(comment_id).orElseThrow(IllegalArgumentException::new);
comment.update(commentDto.getBody());
}
@Transactional
public void delete(Long comment_id, Long post_id) {
Post post = postRepository.findById(post_id).orElseThrow(IllegalArgumentException::new);
commentRepository.deleteById(comment_id);
post.minusCommentNum();
}
}
CommentDto
@Getter @Setter
@AllArgsConstructor
public class CommentDto {
private Long id;
private String body;
private Long member_id;
private String membername;
}
EditCommentDto
@Getter @Setter
@AllArgsConstructor
public class EditCommentDto {
private Long id;
private String body;
}
CommentRepository
public interface CommentRepository extends JpaRepository<Comment, Long> {
}
CommentRepository는 쿼리메서드(ex) findById()...
)만 사용합니다.
MemberService
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
@Transactional
public void save(MemberJoinDto memberJoinDto) {
if (memberRepository.existsByLoginId(memberJoinDto.getLoginId())) throw new IllegalArgumentException();
memberRepository.save(Member.createMember(memberJoinDto.getLoginId(),
bCryptPasswordEncoder.encode(memberJoinDto.getPassword()),
memberJoinDto.getName().strip()));
}
@Transactional
public void delete(Long member_id) {
Member member = memberRepository.findById(member_id).orElseThrow(IllegalArgumentException::new);
List<Comment> comments = member.getComments();
comments.forEach(comment -> comment.getPost().minusCommentNum());
List<Heart> hearts = member.getHearts();
hearts.forEach(like -> like.getPost().minusHeartNum());
memberRepository.delete(member);
}
}
회원을 삭제할 때에는 연관된 Post, Comment, Heart 가 같이 삭제되므로(CascadeType.REMOVE
) 그에 따라 게시글의 댓글 수, 좋아요 수를 수정해야합니다.
회원과 연관된 Comment, Heart를 forEach() 구문을 이용하여 변경 감지기능으로 수정해주었습니다.
MemberJoinDto
@Getter @Setter
public class MemberJoinDto {
private String loginId;
private String password;
private String name;
}
MemberRepository
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByLoginId(String loginId);
boolean existsByLoginId(String loginId);
}
Controller, Service, Repository 개발을 완료했으니 화면에 출력할 HTML 파일을 Thymeleaf와 Bootstrap을 사용하여 만들어주겠습니다.