커뮤니티 프로젝트

좋아요 기능 개발

쭈녁 2022. 12. 12. 20:47

Member 엔티티(일부)

@OneToMany(
        mappedBy = "member",
        cascade = CascadeType.ALL,
        orphanRemoval = true,
        fetch = FetchType.LAZY)
@OrderBy("id desc")
private List<Heart> hearts;

Member 엔티티에 리스트로 Heart 엔티티와 연관관계로 설정하고 연관 관계 주인을 mappedBy로 member로 지정하였다.

cascade = CascadeType.ALL 는 주인인 엔티티가 삭제되었을 때 이와 연관된 엔티티도 삭제되도록 설정한다고 한다. 나의 프로젝트의 경우 Member가 삭제(회원 탈퇴) 되면 좋아요 객체가 연동되어 삭제되게 설정하였다.

 

fetch = FetchType.LAZY

 

지연 로딩(LAZY) 와 즉시 로딩(EAGER) :

 

  • 즉시 로딩(EAGER) : 데이터 조회시 한번에 쿼리를 통해 조인된 데이터를 모두 가져옴
  • 지연 로딩(LAZY) : 해당 객체에 접근할 때 쿼리를 통해 데이터를 가져옴

위 설정에 대해 이해가 잘 가지 않아 많이 고생했던 것 같다. 처음엔 즉시로딩(EAGER)로 설정하여 작성하였었는데 좋아요 객체를 삭제 할 때 객체는 삭제되지 않고 게시글의 좋아요 개수만 내려가는 증상이 나타났다.

이르르 지연로딩(LAZY)로 변경하여 정상화 할 수 있었다.

 

해당 설정에 대한 특징은 한번 더 정리하여 포스팅 해야할 것 같다.

(각 설정시 동작 원리 및 사용 이유는 알겠으나 내 케이스에서 왜 오류가 났었는지는 아직 모르겠음)

 

Heart 엔티티

@Entity
@Data
public class Heart {

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

    private Long postId;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;
}

게시물 번호와 좋아요를 누른 Member를 저장하는 Heart엔티티를 만들었다.

 

HeartRepository

public interface HeartRepository extends JpaRepository<Heart, Long> {
    Optional<Heart> findByMemberAndPostId(Member member, Long postId);
}

JapRepository를 사용하여 save, delete 메소드를 가져다 쓰고

로그인한 사용자가 보고있는 게시물에 좋아요를 눌렀는지 확인하기 위한 findByMemberAndPostId 를 메서드로 추가해주었다.

 

똑똑한 Jpa가 파라미터로 받은 Member와 postId 두값으로 쿼리를 통해 Repository에서 찾아와준다.

HeartService

@Service
@RequiredArgsConstructor
public class HeartService {
    private final SpringDataJpaPostRepository postRepository;
    private final HeartRepository heartRepository;



    public void save(Heart heart, Long postId, Member member)throws IOException{
        if (findHeartWithUserAndPostId(member, postId).isPresent()) {
            throw new IOException("이미 좋아요 표기된 게시물");
        }
        heart.setMember(member);
        heart.setPostId(postId);
        heartRepository.save(heart);

        Post findPost = postRepository.findById(postId).orElseThrow();
        Integer rawHeartCount = findPost.getHeartCount();
        findPost.setHeartCount((rawHeartCount) + 1);
    }

    
    public void delete(Long postId, Member member) throws IOException{
        Optional<Heart> heartWithUserAndPostId = findHeartWithUserAndPostId(member, postId);
        if (heartWithUserAndPostId.isEmpty()) {
            throw new IOException("좋아요된 게시물을 찾을 수 없습니다.");
        }

        heartRepository.delete(heartWithUserAndPostId.orElseThrow());

        Post findPost = postRepository.findById(postId).orElseThrow();
        Integer rawHeartCount = findPost.getHeartCount();
        findPost.setHeartCount((rawHeartCount)-1);
    }

    
    public Integer heartCheck(Long postId, Member member) {
        Optional<Heart> heartWithUserAndPostId = findHeartWithUserAndPostId(member, postId);
        if (member==null||heartWithUserAndPostId.isEmpty()) {
            return 0;
        } else {
            return 1;
        }
    }

    public Optional<Heart> findHeartWithUserAndPostId(Member member, Long postId) {
        return heartRepository.findByMemberAndPostId(member, postId);
    }

}

좋아요를 눌렀는지 확인하는 heartCheck메소드를 먼저 만들었다.

member 객체와 게시물 번호를 받아 두 값이 같은 Heart객체가 있는지 확인하고 없으면 0을 return, 그렇지 않으면(있으면) 1을 return 하도록 작성하였다.

 

save를 통해 좋아요 생성, delete를 통해 좋아요 취소를 구현하였다. 혹시 모를 에러를 위해(좋아요가 눌렸는데 좋아요 요청 or 좋아요가 안눌렸는데 좋아요 취소) IOExecption으로 에러를 던져 주었다.

 

그리고 좋아요 생성은 받은 member와 postId 를 Heart에 저장하나 후 게시물의 좋아요 카운트를 1 높여주었고

좋아요 취소는 member와 postId로 찾아온 Heart객체를 삭제하고 게시물의 좋아요 카운트를 1 낮추었다.

 

Controller

@GetMapping("/post/{postId}")
public String post(@PathVariable Long postId, Model model, Comment comment, @AuthenticationPrincipal PrincipalDetail principalDetail) throws IOException {
    Post post = postService.findById(postId).get();
    model.addAttribute("post", post);
    model.addAttribute("comment", comment);

    if(principalDetail==null){
        Integer heartCheck = 0;
        model.addAttribute("heartCheck", heartCheck);
    }
    else {
        Member member = principalDetail.getMember();
        Integer heartCheck = heartService.heartCheck(postId, member);
        log.info("Check={}", heartCheck);
        model.addAttribute("heartCheck", heartCheck);
    }
    return "post/post";
}

   @Transactional
    @PostMapping("/post/heart/{postId}")
    public String heart(@ModelAttribute Heart heart, @PathVariable Long postId, @AuthenticationPrincipal PrincipalDetail principalDetail) throws IOException {
        heartService.save(heart, postId, principalDetail.getMember());
        return "redirect:/post/{postId}";
    }

    @Transactional
    @DeleteMapping("/post/heart/{postId}")
    public String unHeart(@PathVariable Long postId, @AuthenticationPrincipal PrincipalDetail principalDetail) throws IOException {
        heartService.delete(postId, principalDetail.getMember());
        return "redirect:/post/{postId}";
    }

 

컨트롤러에서는 /post/heart/{게시물번호} 로 오는 각 요청(Post, Delete)을 받아 좋아요 생성/삭제 하도록 작성하였다.

 

그리고 마지막으로 애를 먹었던 게시물 화면의 구현이다. Integer를 값으로 하는 model 'heartCheck'를 만들어 HeartService의 값을 넣어주어 클라이언트 단에 던져주도록 설계하였다 (0이면 좋아요 버튼, 1이면 좋아요취소 버튼)

pricipalDetail의 값이 null로 들어와 NullpointException을 잡는데 조금 고생했는데

pricipalDetail==null일 때 그냥 0으로 값을 주도록 설정하였고, 비로그인으로 좋아요를 누르게 된다면

스프링 시큐리티에서 /post/heart/{게시물번호}의 경로는 로그인한 사용자만 접근 가능하도록 설정하였음으로 로그인 창이 나오게 설계하였다. 로그인을 하게되면 좋아요가 올라가게 된다.

 

좋아요 기능은 개발이 완료되었다. 좋아요 기능을 완료 하였으니 홈 화면에서 인기글, 내가 좋아요 표기한 게시물을 간단하게 리스트 하는 기능을 개발하여 다음 게시물로 올릴 예정이다.

 

 

추가적으로 알게된 점

처음 HeartService를 만들어 실행시킬때 LazyInitializationException 라는 Exception이 터졌다. Controller에서 Heart객체를 가져올 때 코딩을 잘못 작성하여 발생한 예외이고 로직을 다시 수정하여 사라지긴 하였지만, 해당 에러로 오래 고생하고 알아본게 아깝기도 하고 만일 다음에 LazyInitializationException이 다시 발생한다면 참고하기 위해 찾아봤었던 링크를 남겨 본다.(너무 감사합니다.)

 

https://jsonobject.tistory.com/605

 

 

'커뮤니티 프로젝트' 카테고리의 다른 글

이미지 업로드 구현  (0) 2023.01.11
좋아요 List up 구현  (0) 2022.12.13
댓글 삭제 개발  (0) 2022.12.07
댓글 기능 개발 (댓글 저장 부분)  (0) 2022.12.02
회원정보 수정  (0) 2022.11.30