본문 바로가기

프로젝트/멋사 개인 프로젝트 (mutsa-SNS)

[18] mutsa-SNS-2 2일차 - (1) 좋아요 기능 구현 (soft delete 복구)

좋아요 기능을 구현해 보았다.

  • 로그인 한 유저만 좋아요 누르기 가능
  • 이미 좋아요 상태에서 한번 더 누르면 좋아요 취소 (soft delete로 구현)
  • 글에 달린 좋아요 수는 모두 열람 가능

 

기능 구현 중, SQL naming Error 이슈 발생하여 시간을 꽤나 잡아먹었다.

아래에세 좀 더 이야기를 풀어보겠다...

 

1. Domain

Like (Entity)

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Table(name = "likes")
@Entity
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
//soft delete
@SQLDelete(sql = "UPDATE likes SET deleted_at = current_timestamp WHERE id = ?")
//@Where(clause = "deleted_at is null")
public class Like extends BaseEntity{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    private LocalDateTime deletedAt;

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

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "post_id")
    private Post post;

    public void recoverLike(Like like) {
        this.deletedAt = null;
    }

    public static Like toEntity(User user, Post post) {
        Like like = new Like();
        like.setUser(user);
        like.setPost(post);
        return like;
    }

}

like Entity도 ERD의 요청사항에 따라 soft delete 구현을 하였다.

다만, 좋아요를 취소하고 다시 등록 과정에서 LikeRepository에서 좋아요를 불러올 때, @Where annotation을 이용하여deletedAt 처리 된 것을 불러오지 않도록 설정하면, 다시 deletedAt을 null로 되돌리기 힘드니, 위의 설정은 빼주었다.

 

또한, 취소된 좋아요를 다시 복구하기 위한 recoverLike method를 설정하였다.

이 method가 불려오면 deletedAt을 다시 null 상태로 되돌린다.

 

2. Repository

LikeRepository

public interface LikeRepository extends JpaRepository<Like, Integer> {

    Optional<Like> findByUserAndPost(User user, Post post);

    //Like 별칭을 like로 하면 error가 발생한다...
    @Query("SELECT COUNT(l) FROM Like l WHERE l.deletedAt is null and l.post = :post")
    Integer countByPost(@Param("post") Post post);
}

user와 post로 like를 찾는 method를 추가해 주었고,

 

해당 포스트의 좋아요 개수를 count를 해주는 countByPost method를 짤 때, 쿼리문을 직접 짜줬다.

Like Entity의 deletedAt이 null이고 (삭제가 되지 않은 상태. 즉 취소가 되지 않은상태)

Like Entity의 post가 해당 post일때 Like의 개수를 count 해주는 method이다.

 

이 때, Like의 별칭을 like로 해주어 계속 오류가 났었는데, 이에 대해서는 밑에서 좀 더 보충설명을 하도록 하겠다.

 

<Reference>

https://leveloper.tistory.com/103

 

[JPA] JPQL (Java Persistence Query Language)

JPA는 복잡한 검색 조건을 사용해서 엔티티 객체를 조회할 수 있는 다양한 쿼리 기술을 지원한다. 이번 글에서는 다양한 객체지향 쿼리 중 JPQL에 대해 다룰 것이다. JPQL이란? JPQL은 엔티티 객체를

leveloper.tistory.com

https://data-make.tistory.com/614

 

[JPA] JPQL Query 정리

| JPQL(Java Persistence Query Language) - 테이블이 아닌 엔티티 객체를 대상으로 검색하는 객체지향 쿼리- SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않음- JPA는 JPQL을 분석한 후 적절한 SQL을 만

data-make.tistory.com

https://www.w3schools.com/sql/sql_and_or.asp

 

SQL AND, OR, NOT Operators

W3Schools offers free online tutorials, references and exercises in all the major languages of the web. Covering popular subjects like HTML, CSS, JavaScript, Python, SQL, Java, and many, many more.

www.w3schools.com

https://joont92.github.io/jpa/JPQL/

 

[jpa] JPQL

JPA에서 현재까지 사용했던 검색은 아래와 같다. 식별자로 조회 EntityManager.find() 객체 그래프 탐색 e.g. a.getB().getC() 하지만 현실적으로 이 기능만으로 어플리케이션을 개발하기에는 무리이다. 그

joont92.github.io

https://velog.io/@youmakemesmile/Spring-Data-JPA-JPQL-%EC%82%AC%EC%9A%A9-%EB%B0%A9%EB%B2%95Query-nativeQuery-DTO-Mapping-function

 

[Spring Data JPA] JPQL 사용 방법(@Query & nativeQuery & DTO Mapping & function)

JPA Query Method만을 이용해서 작성할 수 없는 SQL를 정의하기 위한 JPQL에 대한 내용을 다루고있습니다.

velog.io

https://wonit.tistory.com/470

 

[배워보자 Spring Data JPA] 쿼리 메서드와 @Query를 이용한 사용자 정의 쿼리

해당 글은 배워보자 Spring Data JPA 시리즈 입니다. 해당 시리즈의 내용이 이어지는 형태이므로 글의 내용 중에 생략되는 말들이 있을 수 있으니, 자세한 사항은 아래 링크를 참고해주세요! Spring Dat

wonit.tistory.com

 

3. Service

PostSservice

@Service
@RequiredArgsConstructor
@Slf4j
public class PostService {

    private final PostRepository postRepository;
    private final UserRepository userRepository;
    private final CommentRepository commentRepository;
    private final LikeRepository likeRepository;
    
    ...

    public String doLike(Integer postId, String userName) {

        Post post = postRepository.findById(postId)
                .orElseThrow(() -> new AppException(ErrorCode.POST_NOT_FOUND, ""));

        User user = userRepository.findByUserName(userName)
                .orElseThrow(() -> new AppException(ErrorCode.NOT_FOUNDED_USER_NAME, ""));

        Optional<Like> like = likeRepository.findByUserAndPost(user, post);

        //좋아요가 이미 존재하고, deletedAt이 null일때 (삭제되지 않은 상태)
        if (like.isPresent() && like.get().getDeletedAt() == null) {
            likeRepository.delete(like.get());
            return "좋아요를 취소했습니다.";
        }

        //좋아요가 있지만, deletedAt이 null이 아닐 때 (삭제된 상태. 즉 취소된 상태)
        if (like.isPresent() && like.get().getDeletedAt() != null) {
            like.get().recoverLike(like.get()); //좋아요 복구 method
            likeRepository.saveAndFlush(like.get());
            return "좋아요를 눌렀습니다.";
        }

        //좋아요가 아예 없을 때 새로 생성
        likeRepository.save(Like.toEntity(user, post));
        return "좋아요를 눌렀습니다.";

    }

    public Integer getLike(Integer postId) {

        Post post = postRepository.findById(postId)
                .orElseThrow(() -> new AppException(ErrorCode.POST_NOT_FOUND, ""));

        return likeRepository.countByPost(post);

    }
}

좋아요를 누를 때 필요한 doLike와 해당 게시글의 좋아요 수를 세는 getLike에 대한 비지니스 로직을 추가하였다.

 

doLike에서 좋아요를 취고하고 누르는 과정을 세가지 조건으로 세분화하였다.

  • 이미 Like Entity가 존재하는 상태이지만, 삭제 되지 않은 상태 (deletedAt == null). 즉 좋아요를 누른 상태 -> 다시 누를 시 좋아요 취소 (deletedAt 에 현재 시간 저장)
  • 이미 Like Entity가 존재하는 상태이지만, 삭제 된 상태 (deletedAt == LocalDateTime). 즉 좋아요가 취소된 상태 -> 다시 누를 시 좋아요 복구(deletedAt = null 로 설정) 
  • Like Entity가 존재하지 않는 상태. -> 누를 때 좋아요 등록 (Like Entity 새로 생성)

 

4. Controller

PostController

@RestController
@RequestMapping("/api/v1/posts")
@RequiredArgsConstructor
public class PostController {

    private final PostService postService;

    ...

    //좋아요
    @PostMapping("/{postId}/likes")
    public Response<String> doLike(@PathVariable Integer postId, @ApiIgnore Authentication authentication) {
        String result = postService.doLike(postId, authentication.getName());
        return Response.success(result);
    }

    @GetMapping("/{postId}/likes")
    public Response<Integer> getLike(@PathVariable Integer postId) {
        Integer result = postService.getLike(postId);
        return Response.success(result);
    }
}

 

 

발생 할 수 있는 Error 들

 

1. Like Entity SQL naming 문제

 

좋아요에 해당하는 Entity의 이름이 Like이기 때문에, SQL table 명을 like로 하려 하였다.

하지만 like는 예약어에 해당되기 때문에, 이름으로 설정 시 error가 발생한다.

아래의 reference에 나와있는 예약어를 피해 이름을 설정하거나, 백틱 ` 으로 감싸서 설정해 주도록 하자.

ex) `like`

 

<Reference>

https://madinthe90.tistory.com/9

 

[MySQL] 예약어 목록 / 예약어를 테이블명, 컬럼명으로 사용시 에러 해결

- MySQL 예약어 목록 ADD ALL ALTER ANALYZE AND AS ASC ASENSITIVE BEFORE BETWEEN BIGINT BINARY BLOB BOTH BY CALL CASCADE CASE CHANGE CHAR CHARACTER CHECK COLLATE COLUMN CONDITION CONSTRAINT CONTINUE CONVERT CREATE CROSS CURRENT_DATE CURRENT_TIME CURRENT

madinthe90.tistory.com

 

2. JPA Repository에서 @Query 사용 시 별칭 문제

 

1번 error의 연장선이다. 

@Query("SELECT COUNT(like) FROM Like like WHERE like.deletedAt is null and like.post = :post")
Integer countByPost(@Param("post") Post post);

위와 같이 쿼리문을 작성하였더니 error가 났다.

원인을 못찾아 where의 문법이 잘못되었나 한참 sql 문법을 찾아보고 jpql 문법을 찾아보았다.

그러던 도중 전혀 문법적 오류가 없는데 생각이 든 찰나 1번 문제가 생각나서 혹시나 하고 like를 l로 바꾸어 실험해 보았는데 역시나였다....

쿼리문 작성시에도 예약어를 column이나 table명으로 사용하지 않도록 주의하자.

 

아래는 발생한 Error message의 일부이다.

Error creating bean with name 'likeRepository' defined in com.example.mutsa_sns.repository.LikeRepository defined in @EnableJpaRepositories declared on JpaRepositoriesRegistrar.EnableJpaRepositoriesConfiguration: Invocation of init method failed; nested exception is org.springframework.data.repository.query.QueryCreationException: Could not create query for public abstract java.lang.Integer com.example.mutsa_sns.repository.LikeRepository.countByPost(com.example.mutsa_sns.domain.Post); Reason: Validation failed for query for method public abstract java.lang.Integer com.example.mutsa_sns.repository.LikeRepository.countByPost(com.example.mutsa_sns.domain.Post)!; nested exception is java.lang.IllegalArgumentException: Validation failed for query for method public abstract java.lang.Integer com.example.mutsa_sns.repository.LikeRepository.countByPost(com.example.mutsa_sns.domain.Post)!

 

<전체 Reference>

https://leveloper.tistory.com/103
https://data-make.tistory.com/614
https://www.w3schools.com/sql/sql_and_or.asp
https://joont92.github.io/jpa/JPQL/
https://velog.io/@youmakemesmile/Spring-Data-JPA-JPQL-%EC%82%AC%EC%9A%A9-%EB%B0%A9%EB%B2%95Query-nativeQuery-DTO-Mapping-function
https://wonit.tistory.com/470
https://madinthe90.tistory.com/9