본문 바로가기
Java/JPA

@Entity, @Query 삽질기

by keunseok 2020. 3. 13.

최근 Spring Boot와 Spring Data JPA를 공부하면서 간단하게 게시판을 작성하는 예제를 살펴보았다. 이 분의 글이 설명도 잘 되어 있는 것 같아서 무작정 따라해보면서 공부하기로 했다.

게시글 입력 기능에 관한 테스트 코드 작성과 기능 확인, 웹 페이지 확인까지 잘 진행되었는데, 게시글 목록에서 문제가 발생했다(미리 밝혀두지만 참고한 글에 오류가 있는 것은 아니다. 내 무지에서 온 것으로 인한 것이다).

아래는 오류 내용이다.

java.lang.IllegalStateException: Failed to load ApplicationContext
  ...
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: 
  Error creating bean ...
  ...
Caused by: org.springframework.beans.factory.BeanCreationException: 
  Error creating bean with name ...
  ...
Caused by: java.lang.IllegalArgumentException: 
  Validation failed for query for method ...
  ...
Caused by: java.lang.IllegalArgumentException: 
  org.hibernate.hql.internal.ast.QuerySyntaxException: 
  ...
Caused by: org.hibernate.hql.internal.ast.QuerySyntaxException: 
  tb_posts is not mapped [SELECT p FROM Posts p ORDER BY p.id DESC]
  ...
Caused by: org.hibernate.hql.internal.ast.QuerySyntaxException:
  Posts is not mapped
  ...

쭈욱 가다가 가장 마지막 부근에서 실제 오류 내용이 나와 있다. @Query 애노테이션에 지정된 구문 문법 오류이고 FROM에 지정되어 있는 tb_posts가 매핑 되어 있지 않다는 내용이다.

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity(name = "tb_posts")
public class Posts extends BaseTimeEntity {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private long id;
  ...
public interface PostsRepository extends JpaRepository<Posts, Long> {
  @Query("SELECT p FROM Posts p ORDER BY p.id DESC")
  Stream<Posts> findAllDesc();
}

예제 코드를 그대로 따라했고 @Query에 지정한 구문에 오타가 있는 것인지 몇 번이고 확인했다. 그렇게 몇 번을 확인하고 실행을 했는데도 불구하고 계속 앞에 나온 예외와 마주쳤다.

검색해보니 @Query에 사용되는 쿼리는 JPQL이기 때문에 FROM에 명기되는 것은 테이블 명이 아닌 엔티티 이름이어야 하고 대소문자에 주의해야 한다는 내용들이 주였다. 그런데 위 코드를 보면 FROM 뒤에 엔티티 클래스 명을 적어 놓았고 대소문자도 제대로 구분해서 일치시켰는데 조차도 계속 예외가 발생해서 멘붕이 오기 시작했다.

그러다가 Stackoverflow에서 원인을 찾아낼 수 있었다.

Update : To be more precise , you should use the entity name configured in @Entity to refer to the "table" , which default to unqualified name of the mapped java class if you do not set it explicitly. (P.S. It is @javax.persistence.Entity but not @org.hibernate.annotations.Entity)

위에 내가 따라하면서 수정한 부분은 @Entity에 name을 이용하여 디폴트를 재정의 한 부분이다. @Entity가 클래스와 테이블명의 불일치 해결을 위해 사용되는 용도로 생각했었는데 잘못 알고 있었던 것이다.

아래와 같이 쿼리를 수정하니 제대로 실행되는 것을 확인할 수 있었다.

public interface PostsRepository extends JpaRepository<Posts, Long> {
  @Query("SELECT p FROM tb_posts p ORDER BY p.id DESC")
  Stream<Posts> findAllDesc();
}

아 이제 된거구나.. 라고 생각했는데 또 무엇인가 잘못된 것을 발견했다. 사실 DB에 생성해놨던 테이블 이름은 tb_post였다. 그런데 @Entity name와 쿼리 FROM 뒤에 적은 것은 tb_posts였다. 어???

원래 의도는 엔티티 클래스 명과 테이블 명이 다른 경우를 가정하고 작업을 진행해본 것이었는데, 의도와는 약간 다르게 동작한 것이다.  정상적으로 실행된 이유를 곰곰이 생각해보니, Spring Boot의 application.properties에서 아래와 같은 설정을 해놓았던 것이다.

spring.jpa.hibernate.ddl-auto=create-drop

이 옵션으로 인해 tb_posts라는 테이블이 생겼다가 사라졌고, SQL 실행 툴을 통해서는 tb_post가 존재하고 있기 때문에 착각을 했던 것이다. 또한 JPQL에서 FROM 뒤에 나오는 엔티티 명이 엔티티 클래스명과 다르다는 것도 마음에 들지 않는다. 

의도했던 몇 가지를 정리해보자.

1. 테이블 이름과 엔티티 명은 상이하므로 이를 반영해야 한다.

2. @Entity에 이름을 부여해서 1번을 맞출 수는 있지만, JPQL에서는 FROM 뒤에 명기되는 엔티티 명이 테이블 명처럼 나타나는 것이 마음에 들지 않는다. 엔티티 클래스 이름과 동일하기를 원한다.

그러다 문득 생각난 것이 @Table이라는 애노테이션이었다. 지금까지 삽질한 것이, 과거 @Table에서 봤던 내용과 @Entity에서 봤던 내용들이 뒤죽박죽 섞여서 머릿 속에 남아 있었던 것이다.

그렇기에 아래처럼 수정해보았다.

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
@Table(name = "tb_post")
public class Posts extends BaseTimeEntity {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private long id;
  ...
public interface PostsRepository extends JpaRepository<Posts, Long> {
  @Query("SELECT p FROM Posts p ORDER BY p.id DESC")
  Stream<Posts> findAllDesc();
}

1번을 충족시키기 위해 @Table 애노테이션을 사용하고 기존에 @Entity에 부여했던 name 속성을 제거하였다.

@Entity 속성을 제거하였으로 이에 따라 @Query의 FROM 뒤에 tb_posts를 엔티티 클래스명인 Posts와 일치하도록 수정하였다.

이렇게 설정을 바꾼 후 테스트 코드를 돌려보니 오류가 발생하지 않는 것을 확인하였다.

어설프게 알고 있다가 날린 시간이 얼마나 되었던지... ㅜㅜ

결론: @Entity의 name을 통해 설정된 엔티티 이름은 JPQL에서 동일하게 사용되어야 한다. 이 사실을 통해 알 수 있는 것은 이 속성은 테이블 이름과의 불일치 용도를 위한 것은 아닌 것으로 판단되며, 이런 목적을 위해서는 @Table을 사용해야 할 것 같다.