우아한테크코스 팀 프로젝트(속닥속닥)에서 사용자(Member)가 해당 게시글(Post)에 좋아요(PostLike)를 눌렀는지 반환을 해줘야 하는 기능이 있었습니다.
@Getter
@Entity(name = "post_likes")
public class PostLike {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "post_likes_id")
private Long id;
@ManyToOne
@JoinColumn(name = "post_id")
private Post post;
@ManyToOne
@JoinColumn(name = "member_id")
private Member member;
}
PostLike 엔티티는 아래와 같습니다. Post와 Member와 연관 관계를 맺고 있습니다.
public interface PostLikeRepository extends JpaRepository<PostLike, Long> {
//...
boolean existsByMemberIdAndPostId(Long memberId, Long postId);
}
따라서, 위와 같이 member가 특정 post에 좋아요(Like)를 눌렀는지 확인하는 메서드가 필요했습니다.
쿼리 실행 결과는 아래와 같았습니다.
select
postlike0_.post_likes_id as col_0_0_
from
post_likes postlike0_
left outer join
member member1_
on postlike0_.member_id=member1_.member_id
left outer join
post post2_
on postlike0_.post_id=post2_.post_id
where
member1_.member_id=?
and post2_.post_id=? limit ?
member_id와 post_id로 조건을 걸고 limit 1을 걸어서 member_id와 post_id와 연관 관계를 가지는 post_likes를 찾는 쿼리입니다.
JPA가 생성하는 Query의 경우에 FK를 통해 조회를 한다면 위와 같이 JPA가 join 쿼리를 실행합니다.
post_likes에 member_id와 post_id가 있기 때문에, join은 불필요하다고 생각해서 직접 쿼리를 작성하려고 했습니다.
JPQL은 EXISTS 쿼리를 지원하지 않기 때문에 아래와 같이 NativeQuery를 작성했습니다.
public interface PostLikeRepository extends JpaRepository<PostLike, Long> {
//...
@Query(value = "SELECT exists (SELECT * from post_likes WHERE member_id = :memberId and post_id = :postId)",
nativeQuery = true)
boolean existsByMemberIdAndPostId(Long memberId, Long postId);
따라서, 아래와 같이 join 없는 쿼리를 실행할 수 있게 되었습니다.
SELECT
exists (SELECT
*
from
post_likes
WHERE
member_id = ?
and post_id = ?)
또한, Test도 잘 통과하였습니다.
그렇게 PR을 merge하고 dev 환경에서도 잘 동작하는지 확인했습니다. 결과는 500(Internal Server Error) 이었습니다.
테스트도 다 통과했는데 무슨 이유일까 의문이 들었습니다.
현재, 테스트 환경에서는 In-Memory DB로 h2를 사용하고 있습니다. 반면, DEV 환경과 운영 환경에서는 MySql을 사용하고 있습니다.
h2의 경우에는 EXISTS 쿼리 실행 결과로 true 혹은 false가 반환되고 Java의 boolean으로 매핑이 됩니다. 공식 문서에도 java의 Boolean에 매핑된다고 나와있습니다.
MySql은 EXISTS 쿼리 실행 결과로 1 혹은 0이 반환됩니다. MySql은 내부적으로 boolean이라는 자료형이 존재하지 않고 참 거짓에 TINY_INT를 사용합니다.
따라서, boolean existsByMemberIdAndPostId
메서드에 EXISTS 쿼리를 실행시켰는데, MySql에서는 1 혹은 0을 반환하고 이를 boolean으로 반환하려고 하니 ClassCastException 발생했던 것이었습니다.
일단, 당장 버그를 잡아야 하기 때문에 JPA가 생성해주는 outer join 쿼리를 사용하도록 해두었습니다. 하지만, 궁극적으로
안정성 측면에서는 가장 훌륭한 방법이라고 생각합니다. 테스트는 h2로 실행하고, 운영은 MySql로 진행되기 때문에 테스트가 100%를 보장해주지 못한다고 생각합니다. 또한, DB를 사용하는 Integration E2E 테스트는 매우 중요합니다. 따라서, Docker를 통해 테스트에서도 운영, 개발 환경과 동일한 환경을 구축해줘야 한다고 생각합니다.
다음에는, Docker로 test를 진행하는 방법에 대해서 알아보겠습니다.
https://www.javatpoint.com/mysql-boolean
http://www.h2database.com/html/datatypes.html
https://phauer.com/2017/dont-use-in-memory-databases-tests-h2/
엔티티를 직접 참조하는 경우 id가 아닌 엔티티로 걸어줘야 합니다~ id로 조회하게되면 member 와 post 필드에있는 id 값을 가져와서 비교하겠다는 뜻이어서요