Hibernate 의존성 버그
문제 상황
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Entity
@IdClass(AId.class)
public class A {
@Id
@ManyToOne
private User user;
@Id
@ManyToOne
private User follower;
static public class AId implements Serializable {
Long user; // User의 ID는 Long이다.
Long follower;
}
}
문제를 알아보기 앞서 엔티티 A
는 위와 같은 형태로 다중 FK를 묶어 PK로 사용한다.
모든 컴포넌트를 띄운뒤 인수 테스트를 진행하던 중 테스트에 빨간불이 들어왔다. 이유는 서버가 INTERNAL SERVER ERROR
를 응답하는 것이었는데, 해당 메시지는 서비스에서 처리되지 않은 에러일 경우에만 발생한다. 즉, 무언가 처리되지 않은 예외가 발생했다.
특이 사항으론 실제 로컬 서버를 띄워 요청을 하면 예외가 발생하지 않는 것이 있었다. 로컬 서버와 인수 테스트 환경의 차이는 DB 종류가 유일하다. 로컬서버는 MariaDB
를 테스트 환경에선 H2
인메모리 DB를 사용하고 있었다. 이를통해 DB와 관련된 로직에서, 특히 하이버네이트와 관련하여 문제가 발생하고 있음을 유추했다.
디버깅을 통해 확인해본 결과 다음 메서드 안에서 예외가 발생하고 있었다.
1
aRepository.existsByUserIdAndFollowerId(...);
일단 디버깅을 통해 던져지는 예외의 종류를 살펴보았다.
위와 같이 AssertionError
가 발생하고 있었다. AssertionError
는 별도의 추가적인 예외 메시지가 없어 위 화면만 보고는 해결하기 난감한 상황이었다.
일단 예외가 발생하는 코드를 찾기위해 모든 Exception
에 브레이크포인트를 지정하고 디버깅을 재개했다. Repository
는 JPA가 구현해주고 중간중간 프록시, 요청 위임이 많기 때문에 코드의 흐름을 일일이 따라가기엔 어려웠기 때문이다.
가장 먼저 발생하는 예외는
NoSuchMethodException
으로 existsByUserIdAndFollowerId
를 찾지 못한다는 내용이다.
참고로 JPA는 내부적으로 프록시와 Reflection API를 사용하기 때문에
Class.java
에서 에외가 잡힌 것이다.
AssertionError
는 위 코드에서 발생하고 있었다. 앞서 existsByUserIdAndFollowerId
를 찾지 못했기 때문에 assert
문이 실패하는 것이다.
SQM은 Semantic Query Model의 약자로 하이버네이트 JPA 구현체에서 사용하는 일종의 쿼리 파서다. JPQL, Criteria와 같은 쿼리를 SQM으로 번역한뒤 SQM을 SQL로 파싱하여 실제 쿼리를 처리한다.
해결 방법
디버깅을 통해 얻은 키워드들을 조합하여 이리저리 검색해보았다. 그 중 유의미한 결과를 낸것은 jpa sqm AssertionError
검색어 였다. 최상단에 다음과 같이 공식 repo에 Issue가 등록되어 있었다.
Issue의 문제상황은
@IdClass
를 사용한다.- 테스트 중에만 문제가 발생한다.
- 예외 메시지가 거의 동일하다.
- 발생한 의존성 버전과 발생한 날짜가 유사하다.
로 나와 매우 비슷한 문제 상황이었다. 커멘트에는 비슷한 문제를 겪은 사람이 여러명이 있었고 JPA 메인테이너는 하이버네이트에서 발생한 문제이며, 관련 이슈는 https://hibernate.atlassian.net/browse/HHH-16988 로 가보라는 댓글을 달았다.
하이버네이트 이슈에서 설명하는 상황은 나와는 살짝 달랐으나 기본 골격은 동일하다고 생각했다. 해당 버그는 6.2.5
버전에선 발생하지 않고 있으며 6.2.6
부터 문제가 발생한다. 내 프로젝트의 의존성도 6.2.6
버전을 사용하고 있었다.
다행히 해당 버그는 우선순위가 주요로 측정되어 발견한지 2주도 안되어 해결되어 있었다. 문제가 발생하는 hibernate-core
의 의존성 버전을 문제가 해결된 6.2.8
로 올렸다.
그리고 테스트에 초록불이 들어왔다.
마치며
의존성 버전 관리의 중요성을 다시 한번 느꼈다. (왜 회사에선 최신 버전을 안쓰고 레거시 버전을 그대로 쓰는지도 한번더 이해가 됐다..)
사실 간단한 버그였지만 디버깅 과정이 나름 재밌던 버그라 글로 작성했다. 모든 예외에 브레이크 포인트를 거는 기능도 알고는 있었지만 이번에 처음 써봤다. 옛날엔 버그가 발생하면 무작정 구글에 검색을 하였는데 요즘은 디버깅을 통해 직접 버그를 찾아 해결하거나(에러 메시지가 생각보다 매우 친절한 것도 느낀다) 키워드를 함축한 후 검색을 하고 있다.
옛날엔 개발자의 기본 소양에 구글 검색이라고 답했는데 다시 묻게 된다면 공식문서, 책, 컨퍼런스를 읽고 듣기 위한 영어와 디버깅 능력을 답할 것 같다.