spring boot,jpa 좋아요 토글방식 구현

디하·2023년 10월 29일
5

bucket📦

목록 보기
3/10
post-thumbnail

좋아요(❤️) 기능 구현

  • 보통 좋아요 기능은 좋아요를 누르면 좋아요 데이터가 생성되고,
    좋아요 취소를 누르면 데이터가 삭제되고, 다시 좋아요를 누르게 되면 좋아요가 다시 생성되는 식으로 기능이 구현이 되는 자료들
    을 많이 보게 되었다
  • 우리 팀 프로젝트에서는 좋아요 기능을 토글 형식으로 기획을 하였고, 소프트 딜리트 정책을 세웠다

  • 데이터가 계속 쌓이는 형식이 아닌 좋아요 상태만 변경하는 식으로 관리하기 위해서 이렇게 정책을 세우고 기능을 구현하였다


1. 좋아요 토글 방식 구현

특징: 하나의 api안에서 좋아요 생성, 취소 , 다시 생성을 하나로 처리하기

1)Controller

  • postMapping으로 좋아요 생성 , 취소, 다시 생성을 할 수 있게 코드를 구현
@PostMapping("/like")
    public ResponseEntity<ResultDto<Void>> toggleHeart(@RequestBody HeartRequestDto heartRequestDto) {

        CommonResponseDto<Object> addHeart = heartService.toggleHeart(heartRequestDto);
        ResultDto<Void> result = ResultDto.in(addHeart.getStatus(), addHeart.getMessage());

        return ResponseEntity.status(addHeart.getHttpStatus()).body(result);
    }

2)HeartRequestDto

@Getter
@NoArgsConstructor
public class HeartRequestDto {

    private Long userId;
    private Long posterId;

    @Builder
    public HeartRequestDto(Long userId, Long posterId){
        this.userId = userId;
        this.posterId = posterId;
    }


    //좋아요 처음 생성
    public Heart toAddHeartEntity(User user, Poster poster){
        return Heart.builder()
                .user(user)
                .poster(poster)
                .status(true)
                .build();
    }


 
}

3)Service

서비스에서 가장 고민이 크게 되었다
하나의 postmapping에서 좋아요생성,삭제,다시생성을 진행해야하는데 그렇다면 어떻게 조건식으로 나눌지 고민이 많았다

  1. 기존에 좋아요 데이터가 없다면 -> 좋아요를 생성
  2. 기존에 좋아요 데이터가 있다면 -> 좋아요 취소, 다시생성

이렇게 흐름을 잡았다

 Heart existingHeart = heartRepository.findUserAndPoster(userId, posterId);


        if (existingHeart == null) {
            //기존 데이터가 없다면 1. 좋아요 데이터 생성

            

        } else {
            //기존에 데이터가 있다면 2. 좋아요 상태를 변경


            if (existingHeart.getStatus()) {
                //status:true -> false ( 좋아요 취소 )
        

            } else {
				//status:false->true(좋아요 다시 생성)
            }
        }


하지만 여기서 큰 고민이 생겼다
좋아요 취소 당시 삭제가 아닌 좋아요 상태값만 변경을 하고 다시 생성시 좋아요 상태값만 변경하게 만들어야한다는 점이였다

소프트 딜리트 쪽에서 고민이 생겨버린 것이다


2. 좋아요 소프트 딜리트 정책

지금까지 프로젝트를 진행하면서 하드딜리트의 방식으로 구현해왔었다
솔직히, 하드딜리트, 소프트딜리트에 대해 깊이 있게 생각하지 않아왔기 때문에 이번 프로젝트를 통해서 이 둘의 장단점도 공부할 수 있었다

하드 딜리트와 소프트 딜리트를 간단히 비교

  • hard delete: 테이블에서 직접적으로 제거
  • soft delete: UPDATE 명령어를 사용하여 삭제 여부를 알수 있는 컬럼에 데이터가 삭제되었다는 값을 넣어서 표현

물론, 이번 프로젝트에서 soft delete를 사용하게 된 목적은 좋아요 데이터를 삭제하지 않고 안에 상태값만 변경하게 하여 user와poster의 데이터를 다시 또 저장하는 번거로움을 없애고 싶었기 때문이다


3. 좋아요 상태값만 변경하기

좋아요 상태값만 변경을 하기 위해 처음에는 setter를 이용하여 기능을 구현하였다

하지만, setter를 사용하명 안되는 이유가 있다는 것을 알게 되었다

  • setter 메소드를 사용하면 값을 변경한 의도를 파악하기 힘들다
  • 객체의 일관성을 유지하기 어렵다

1.setter 메소드를 사용하면 값을 변경한 의도를 파악하기 힘들다


public Heart updateHeart(long id) {
    final Heart heart = findById(id);
    heart.setStatus("true");
    heart.setUser(User);
    heart.setPoster(Poster);
    return heart;
}

2.객체의 일관성을 유지하기 어렵다

자바 빈 규약을 따르는 Setter는 public으로 언제든지 변경할 수 있는 상태가 된다 위처럼 좋아요 변경 메소드뿐만아니라 모든 곳에서 좋아요의 상태를 변경 할 수 있는 상태가 되기 때문에 객체의 일관성을 유지하기 어려움

setter가 아닌 다른 방법이 없을까?
=> builder 사용하기

-빌더패턴은 요구사항에 맞게 필요한 데이터만 이용하여 유연한 클래스 생성이 가능 그렇기 때문에 다양한 생성자들이 사라지고 전체 생성자 하나만을 가지고 있는 형태로 변경되어 유지보수 및 가독성이 향상 또한 객체를 생성할 때 인자 값의 순서가 상관없다는 장점이 있다


service 에 builder 적용

  1. newHeart 빌더로 생성해서, user,poster에서 userid를 받아와 저장하고 status는 true로 설정해준다

  2. 기존 데이터가 있다면 heartRepository.updateStatus(heartId,true);
    으로 status 값만 변경해준다


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

    private final UserRepository userRepository;

    private final PosterRepository posterRepository;

    private final CommonService commonService;


    @Transactional
    public CommonResponseDto<Object> toggleHeart(HeartRequestDto heartRequestDto) {
        Long userId = heartRequestDto.getUserId();
        Long posterId = heartRequestDto.getPosterId();

        //유저 존재 여부 확인
        User user = userRepository.findById(userId)
                                  .orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND));

        //게시글 존재 여부 확인
        Poster poster = posterRepository.findById(posterId)
                                        .orElseThrow(() -> new NotFoundException(ErrorCode.POSTER_NOT_FOUND));

        // 기존 좋아요 존재
        Heart existingHeart = heartRepository.findUserAndPoster(userId, posterId);


        if (existingHeart == null) {
            //기존 데이터가 없다면 1. 좋아요 데이터 생성

            Heart newHeart = new Heart().builder()
                                        .status(true)
                                        .user(user)
                                        .poster(poster)
                                        .build();

            poster.heartIncrease();
            heartRepository.save(newHeart);

        } else {
            //기존에 데이터가 있다면 2. 좋아요 상태를 변경
			Long heartId = existingHeart.getId();	

            if (existingHeart.getStatus()) {
                //status:true -> false ( 좋아요 취소 )
                poster.heartDecrease();
                heartRepository.updateStatus(heartId,false);
                
                

            } else {
                poster.heartIncrease();
                heartRepository.updateStatus(heartId,true);
               
            }
        }


        return commonService.successResponse(SuccessCode.HEART_TOGGLE_SUCCESS.getDescription(), HttpStatus.CREATED, null);
    }

repository

  1. 문제점: setter를 사용하지 않고 builder로 저장을 해주려고 했는데 계속 저장이 되지 않는 문제점을 알게 되었다 (status가 변경이 되지 않음)
    그래서 어떻게 이 방법을 해결할지 고민을 하다가 repository쪽에서 수정 쿼리를 작성해보면 좋지 않을까 생각이 들었다

-> JPA 에서 변경하고 싶은 컬럼만 변경하려면 어떻게 해야할까?의 고민


save()가 아닌 @Modifying 사용하기

@Modifying

@Query Annotation으로 작성 된 변경, 삭제 쿼리 메서드를 사용할 때 필요(INSERT, UPDATE, DELETE, DDL 에서 사용)
-> 벌크 연산 시 사용됨

기본적으로 제공되는 쿼리 메서드나 메서드 네이밍으로 파생되는 쿼리 메서드는 적용되지 않는다 -> 영속성 콘텐스트 관리에 주의


벌크 연산이란?

벌크연산: 여러건의 UPDATE,DELETE연산을 하나의 쿼리로 하는 것을 의미

JPA에서 하나의건 (단건) UPDATE는 Dirty Checking을 통해서 수행, save()로도 가능 / DELETE 는 하나/여러건 모두 쿼리메서드로 제공됨

-> 여기서의 문제점

JPA에서는 영속성 컨텍스트에 있는 1차 캐시를 통해 entity를 캐싱하고, DB 접근 횟수를 줄임으로써 성능개선을 한다
1차 캐시는 @Id값을 key값으로 entity를 관리한다

그래서 findById등을 통해 entity를 조회했을 시 @Id값이 1차 캐시에 존재한다면 DB에 접근하지 않고 캐싱된 entity를 반환한다

그렇다면 벌크연산(UPDATE를 @Modifying을 이용)을 통해 데이터 변경쿼리를 실행하고, 해당 entity를 조회한다면
1차 캐시에 있는 entity를 반환하게 될 것
이다
(즉, 변경쿼리가 제대로 실행이 되지 않는다)

-> 벌크연산 실행시 1차 캐시(영속성컨텍스트)와 DB의 데이터 싱크가 맞지 않게 되는 것

해결책: @Modifying(clearAutomatically = true)

@Modifying의 clearAutomatically인 default =false인 값을 true로 변경해 준다면 벌크 연산 직 후 자동으로 영속성 컨텍스트(1차캐시)를 clear 해주게 되어 데이터 동기화 문제를 해결할 수 있게 된다


public interface HeartRepository extends JpaRepository<Heart, Long> {

    @Query("SELECT h FROM Heart h "
            + " LEFT JOIN h.user u "
            + " LEFT JOIN h.poster p "
            + " WHERE u.id = :userId AND p.id = :posterId")
    Heart findUserAndPoster(Long userId, Long posterId);


    @Query("UPDATE Heart h "
            + " SET h.status = :status "
            + " WHERE h.id = :id")
    @Modifying(clearAutomatically = true)
    void updateStatus(@Param("id") Long heartId, @Param("status") boolean b);

}

출처:
https://devhyogeon.tistory.com/4
https://joojimin.tistory.com/71

profile
🖥️ ⌨️🖱️🩵

0개의 댓글

관련 채용 정보