JAVA/JPA 학습기록

[ERROR] JPA initializationError 해결 방법 모음 및 대처법

kth990303 2023. 2. 10. 12:15
반응형

스프링 프레임워크에서 JPA ORM을 이용하면 반드시 한 번쯤은 만날 수밖에 없는 에러가 있다.

해당 에러가 발생하면서 테스트가 실패하거나, 혹은 애플리케이션 실행이 안될 수 있다

could not prepare statement; SQL; nested exception is org.hibernate.exception.SQLGrammarException: could not prepare statement

 

org.springframework.dao.InvalidDataAccessResourceUsageException: could not prepare statement; nested exception is org.hibernate.exception.SQLGrammarException: could not prepare statement

at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException

(HibernateJpaDialect.java:259)

 

해당 에러는 굉장히 범용한 에러라서 수많은 원인들이 존재한다.

당장 구글링해보면 원인도, 해결책도 굉장히 많다. 나에게 직접적으로 맞는 해결책을 찾기란 굉장히 어렵다.

위에서 발생한 에러 해결법 역시 수많은 케이스 중 일부일 뿐이다.

 

명탐정 코난

그렇기 때문에 이번 포스팅에선 직접적인 해결책을 소개하기에 앞서, 위 에러가 발생했을 때 어떻게 하면 될지에 대해 작성해보려 한다.

 

이 포스팅을 보는 여러분들과 나는 명탐정이 되어 에러를 발생시키는 원인 범위를 점점 좁혀나가 에러 원인을 찾아나갈 것이다.

여러분들이 겪는 에러의 원인은 이 포스팅에서 맨 윗부분이 될 수도, 맨 마지막 부분이 될 수도, (심지어 정말 마이너한 에러라면) 아예 안나올 수도 있다. 

 

하지만 이 포스팅의 중점은 직접적인 에러 해결책보다는, 에러를 찾아나가는 방법에 맞춰져 있다는 점을 기억하면 좋을 듯하다.

 

환경

  • Spring Boot 2.7.7
  • JPA
  • Gradle
  • kotlin 1.6.21
  • jvmtarget 11
  • test DB: H2 

1. 맨 위 에러만 보지 말고, 밑으로 내려보자

보통 맨 위에 띄워주는 에러 메시지가 비교적 디테일하게 나와 쉽게 해결한 경험도 있을 것이다.

하지만 우리가 지금 보는 에러메시지는 굉장히 포괄적인 편.

따라서 이런 경우엔 밑으로 내려주자.

 

에러 메시지

1
2
3
4
5
6
7
8
9
10
11
could not prepare statement; SQL [insert into member (id, email, gender, has_changed_password, name, value, phone_number, status) values (default, ?, ?, ?, ?, ?, ?, ?)]; nested exception is org.hibernate.exception.SQLGrammarException: could not prepare statement
org.springframework.dao.InvalidDataAccessResourceUsageException: could not prepare statement; SQL [insert into member (id, email, gender, has_changed_password, name, value, phone_number, status) values (default, ?, ?, ?, ?, ?, ?, ?)]; nested exception is org.hibernate.exception.SQLGrammarException: could not prepare statement
    at app//org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:259)
 
Caused by: org.hibernate.exception.SQLGrammarException: could not prepare statement
    at app//org.hibernate.exception.internal.SQLExceptionTypeDelegate.convert(SQLExceptionTypeDelegate.java:63)
    
 
Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "MEMBER" not found; SQL statement:
insert into member (id, email, gender, has_changed_password, name, value, phone_number, status) values (default, ?, ?, ?, ?, ?, ?, ?) [42102-214]
    at app//org.h2.message.DbException.getJdbcSQLException(DbException.java:502)
cs

at 으로 시작하는 메시지는 Caused by 바로 밑에 뜬 부분을 제외하곤 지워주었다.

대부분의 경우, at 으로 시작하는 메시지를 일일이 볼 필요는 없다.

대신 밑으로 내려서 Caused by 로 시작되는 것들은 다 확인을 해볼 필요가 있다!

 

잘 보면 나의 경우는 SQLGrammarException이 발생한 것이며,

JdbcSQLSyntaxErrorException: Table "MEMBER" not found인 상황이다.


2. application.yml 살펴보기

여기(application.yml)서 보통 절반 이상은 에러 원인을 검출해낼 수 있다.

Table "Member" not found라면 member 테이블이 생성 혹은 존재하지 않아서 발생한 에러이다.

 

application.yml 설정에서 jpa.hibernate.ddl-auto: validate 또는 none 이라면 없는 DB table이 따로 생성되지 않는다.

이 경우는 DB table을 생성해주거나 ddl-auto를 create 또는 create-drop으로 바꿔주면 된다.

 

application.yml

하지만 위와 같이 ddl-auto: create-drop 인 경우에도 불구하고 애플리케이션 실행, 혹은 테스트 실행 시에 Table Not Found 에러가 뜰 수 있다.

 

그런 경우는 이러한 경우를 의심해볼 수 있다.

  • 띄어쓰기가 1칸이 아닌 2칸 이상이 돼있는 경우
  • 들여쓰기가 잘못돼있는 경우
  • 오타가 있는 경우(ex. spring.datasource.url이 아닌 spring.datasoucre.url)
  • spring.datasource.url이 잘못된 경우
  • url에서 MODE=MYSQL; 이 필요한 경우인데 쓰지 않았거나 그 외에 옵션들이 누락된 경우

 

자, 여기서 문제를 발견했다면 다행이다.

하지만 잘 찾고 봐도 오타가 보이지 않는 경우가 있거나, 해당 문제점을 고쳐도 제대로 동작하지 않을 수 있다.


3. 애플리케이션을 직접 실행해보고 h2-console로 들어가보자

한번 애플리케이션을 직접 실행해보자.

만약 직접 실행조차 되지 않는다면 2번에서 웬만한 문제는 해결이 돼야 하는 경우인 것.

 

정말 신기하게도 현재 이 포스팅에서 다루는 에러는, 애플리케이션 자체는 실행이 잘 되는 모습이다.

테스트 실행 시에 특수한 경우에서만 Initialization Error가 발생한다는 것.

 

한번 h2-console을 띄워주기 위해 yml 파일에 아래 코드를 추가하여 다시 실행해보자.

spring:
  h2:
    console:
      enabled: true

h2-console (localhost:8080/h2-console) 을 띄워서 테이블 생성 여부를 눈으로 확인할 수 있도록 해주는 코드이다.

 

URL이랑 name, password를 yml 파일에 설정해준대로 잘 들어가주고~

Connect를 누르면 연결이 돼야 정상이다. (만약 안된다면 yml 설정파일 오류를 의심해보거나, test.mv.db 파일 문제이니 터미널 홈으로 들어가서 test.mv.db를 지워주고 다시 해보자.)

 

자, h2-console에 연결했다면 왼쪽에서 테이블 생성 여부를 확인해보자.

 

1.

만약 모든 테이블이 생성이 안됐다면 yml 파일 설정을 의심해볼 수 있다.

또는 ddl-auto: validated 또는 none이라는 조건 하에 schema.sql 문법이 잘못됐을 가능성도 크다. 발생한 에러가 SQLSyntaxError이기 때문.

 

2.

만약 모든 테이블이 생성이 됐거나, 프로덕션 환경 자체에는 아예 문제가 없다면?

그렇다면 테스트에서만 문제가 있는 것이다. src/test/resources에 테스트용 schema.sql이 잘 적용이 됐는지, @Sql 문법이나 Config 세팅엔 문제가 없는지 확인해봐야 한다. 또는 스프링 빈으로 필요한 객체들은 잘 주입이 됐는지 확인해볼 수 있겠다.

 

3.

하지만 나의 경우는 웬만한 테이블은 생성이 제대로 됐는데 Member랑 Administrator 테이블만 생성이 안되는 케이스였다.

즉, Member 엔티티랑 Administrator 엔티티 코드에 무언가 문제가 있어서 ORM(여기서는 JPA)이 제대로 매핑해주지 못했다는 것!

일단 member, administrator라는 테이블명이 SQL에서 예약어라면 생성이 안될 수 있다.

하지만 이 단어들은 SQL 예약어도 아니다!

 

실제로 애플리케이션 실행 시 발생한 에러메시지를 보면 더더욱 명확해진다.

 

Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement 

"\000a    create table member (\000a       id bigint generated by default as identity,\000a        email varchar(255) not null,\000a        gender varchar(10) not null,\000a        has_changed_password boolean not null,\000a        name varchar(255) not null,\000a        [*]value varchar(255),\000a        phone_number varchar(15) not null,\000a        status varchar(15) not null,\000a        primary key (id)\000a    )"; 

expected "identifier"; SQL statement:

    create table member (
       id bigint generated by default as identity,
        email varchar(255) not null,
        gender varchar(10) not null,
        has_changed_password boolean not null,
        name varchar(255) not null,
        value varchar(255),
        phone_number varchar(15) not null,
        status varchar(15) not null,
        primary key (id)
    ) [42001-214]

이제 슬슬 원인이 좁혀지기 시작한다.


4. Entity 코드를 ORM 규칙대로 잘 사용했는가

ORM이 매핑을 못하는 데엔 이유가 있을 것이다.

JPA를 사용한다면 @Entity를 붙여주었는지, @Column을 각 컬럼에 맞게 붙여주었는지 등등이 문제가 될 수 있겠다.

연관관계 (@ManyToOne, @OneToOne) 세팅은 잘 됐는지 확인해볼 수 있을 듯.

 

자, 한번 이번 포스팅에서 사용된 Member 코드를 보자.

Member 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Entity
@Table(name = "member")
class Member(
        @Column(name = "name", nullable = false)
        var name: String,
 
        @Column(name = "password", nullable = false)
        var password: Password,
 
        @Column(name = "email", nullable = false)
        var email: String,
 
        @Enumerated(EnumType.STRING)
        @Column(name = "gender", nullable = falselength = 10)
        var gender: Gender,
 
        @Column(name = "phone_number", nullable = falselength = 15)
        var phoneNumber: String,
 
        @Enumerated(EnumType.STRING)
        @Column(name = "status", nullable = falselength = 15)
        var status: MemberStatus = MemberStatus.NOT_COMPLETION,
 
        @Column(name = "has_changed_password", nullable = false)
        var hasChangedPassword: Boolean = false,
)
cs

자, 일단 문제가 될만한 부분은 전혀 보이지 않는다.... 가 아니라!!

우리 코드에선 Password 값 객체를 사용중이었으며, 해당 객체에는 @Embedded 어노테이션이 붙여져있다.

 

그렇기 때문에 password는 아래 코드처럼 돼야 JPA가 Password랑 매핑해서 연결해줄 수 있다!

1
2
3
@AttributeOverride(name = "value", column = Column(name = "password", nullable = false))
@Embedded
var password: Password,
cs

 

이렇게 ORM (여기서는 JPA) 규칙을 놓친 부분이 있는지, 요구사항과 기획 문서를 꼼꼼히 체크해보면서 다시 한 번 확인해보자.

 

테스트 All passed
테이블 생성 잘 됨.

 

 

해결 완료

원인이 굉장히 많이 보이는 에러메시지라 하더라도, 결국 내 상황에 처한 문제원인은 하나이다.

따라서 무분별한 구글링보다는 단서를 찾아나가면서 차분히 원인분석을 하거나, 공식문서를 꼼꼼히 읽는 것이 중요할 듯하다.

또는 트러블슈팅 포스팅을 보다 하더라도, 최대한 꼼꼼히 보는 것이 중요할 듯.


해당 포스팅에서 다룬 에러는, Entity 내 코드에서 문제가 있었다.

그렇지만 그 외에 @Transactional 관련 에러일 수도 있고, build.gradle (또는 build.gradle.kts, 또는 pom.xml)에 의존성이 주입이 되지 않을 수도 있고 다양한 원인들이 존재할 수 있다.

 

결론은!

  1. 에러 메시지는 밑으로 내려서 Caused by 로 시작되는 것들은 다 확인을 해볼 필요가 있다.
  2. 애플리케이션 실행, 테스트코드 실행을 직접 해보고 console에서 확인을 해보면서 단서를 찾아가자.
  3. 요구사항이나 로직 상에 놓친 부분 (ex. ORM 규칙 누락, SQL 예약어 사용, VO 객체 사용임에도 불구하고 단순 primitive형 타입 사용 등)이 있는지 체크하자.
  4. 구글링으로 찾을 수 있는 것들은 최대한 찾아보고, 문서들을 대충 읽지 말고 꼭 꼼꼼히 읽어보자!

 

반응형