DB 환경 차이로 인한 ClassCastException(feat. mysql은 boolean을 반환하지 않는다)

공병주(Chris)·2022년 10월 6일
0
post-thumbnail

우아한테크코스 팀 프로젝트(속닥속닥)에서 사용자(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를 찾는 쿼리입니다.

불필요한 join 같은데?

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) 이었습니다.

테스트도 다 통과했는데 무슨 이유일까 의문이 들었습니다.

MySql에는 boolean이 없다

현재, 테스트 환경에서는 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 쿼리를 사용하도록 해두었습니다. 하지만, 궁극적으로

docker를 사용해서 테스트 환경도 MySql로 통일하자

안정성 측면에서는 가장 훌륭한 방법이라고 생각합니다. 테스트는 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/

2개의 댓글

comment-user-thumbnail
2022년 10월 6일

엔티티를 직접 참조하는 경우 id가 아닌 엔티티로 걸어줘야 합니다~ id로 조회하게되면 member 와 post 필드에있는 id 값을 가져와서 비교하겠다는 뜻이어서요

1개의 답글