Liquibase의 table not found 에러와 @Lob
서론
이전에 기본적인 api를 개발할때 분명히 postman으로 잘 동작했던 코드가
클라이언트 사이드에서 동작하지 않는 것을 발견했다.
이건 말이 안되는 상황이라고 생각했고
이번에 개발하는 api에 대해서는
- domain
- repository
- service
- controller
별로 테스트를 작성하고
클라이언트 작업으로 넘어가야겠다고 마음을 먹었다.
기본적인 domain 테스팅 이후
repository => service => controller 순으로 테스팅 계획을 세웠다.
@DataJpaTest를 사용해 repository 테스팅을 진행하던 중
2가지 문제가 발생했다.
Table "CATEGORY" not found
다음은 Category.java라는 새로운 Entity를 생성하고
jpa-buddy를 사용해 생성된 파일의 일부를 가져와봤다.
<!-- 02-db.changelog.xml -->
...
<changeSet>
<addColumn tableName="video">
<column name="category_id" type="BIGINT"/>
</addColumn>
<addForeignKeyConstraint
baseColumnNames="customer_id"
baseTableName="category"
constraintName="FK_CATEGORY_ON_CUSTOMER"
referencedColumnNames="id"
referencedTableName="customer"
/>
</changeSet>
<changeSet>
<addForeignKeyConstraint
baseColumnNames="category_id"
baseTableName="video"
constraintName="FK_VIDEO_ON_CATEGORY"
referencedColumnNames="id"
referencedTableName="category"
/>
</changeSet>
...
이후 gradle 빌드는 당연히 정상적으로 동작했고
이제 만들어둔 test를 실행하자 다음과 같은 에러 로그를 뱉으며
crash 하게 되었다.
Reason: liquibase.exception.DatabaseException: Table "CATEGORY" not found;
SQL statement:
ALTER TABLE PUBLIC.category
ADD CONSTRAINT FK_CATEGORY_ON_CUSTOMER FOREIGN KEY (customer_id)
REFERENCES PUBLIC.customer (id)
어이가 없었다.
- docker 컨테이너로 확인해봐도
- intellij database로 확인해봐도
category 테이블은 제대로 생성되어있었다.
그러면 인메모리에서 h2로 테스트를 돌릴 때도
당연히 생성해서 진행돼야 하는 거 아닌가?
한편, 에러로그는 정확한 것이 실제로 위 마이그레이션 파일에는
<createTable tableName="category">
...
</createTable>
이 부분이 정말로 빠져있었다.
- 이게 빠져있는데 gradle빌드는 지금까지 어떻게 동작했던 것인가?
- 실제 table생성만 하고 마이그레이션 파일에는
create table을 왜 만들어 주지 않은 것인가?
이 지점에 대한 의문점을 해소하지 못했다.
의문점을 해소하지 못했기 때문에 글로 남기는 것이기도 하다.
해결한 방법
1. create table xml 추가
직접 xml 파일에 들어가 intellij 위쪽상단의
add linquibase change > create > table : category를 눌러서 추가했다
(자동으로 위에서 언급한 create table xml을 만들어준다)
2. checksum 문제 해결
위와 같이 manual 수정을 하게 되면
liquibase가 내부적으로 관리하는 database_changelog 테이블의
md5sum(checksum)과 일치하지 않게 되어서
아래와 같은 에러를 뱉는다.
Validation Failed:
1 change sets check sum
com/example/changelog.xml::1::example was:
8:63f82d4ff1b9dfa113739b7f362bd37d
but is now: 8:b4fd16a20425fe377b00d81df722d604
문서에서는 여러 방법을 소개한다
- Database Changelog
- validCheckSum
- clear-checksums
이 중에서 1번을 택했다.
마이그레이션 파일의 validChecksum을 계속 추가하면서
가독성을 떨어뜨리고 싶지 않았기 때문이다.
문제는 당연히 존재한다.
예상이지만 현재 RDS에 올라가있는 db에서도
첫번째 build를 할 경우
jdbc가 연결될 때 이와 동일하게 crash가 발생할 거라고 본다.dev환경에서 위와 같이 해결했기 때문에
datagrip으로 똑같이 해결해 줘야 할 것이다.
이 과정에서 휴먼에러가 발생할 수 있다.이런 지점에서 생각해보면 3번이 제일 괜찮을 수 있다는 생각이 든다.
현재로서는 모든 걸 지우고 다시 체크섬을 부여했을 때
발생할 수 있는 side-effect가 떠오르지 않는다.
1번을 택했다면 해결방식은 간단하다.
- intellij database > database_changelog 테이블에 진입
- 문제가 되는 old checksum을 locate
- validation failed 로그에서 보여준 새로운 checksum으로 교체
- 변경사항을 submit
이게 끝이다.
columnDefinition="TEXT" 와 @Lob
Video.java라는 entity에서는
아래와 같은 컬럼 필드를 선언하고 있다.
@Column(name = "url", nullable = false, columnDefinition = "TEXT")
private String url;
url 길이가 VARCHAR(255)를 넘어가는 상황은
반드시 발생할 거라고 생각했고,
그래서 columnDefinition="TEXT" 을 지정했던 것이다.
이 역시도 테스트 코드를 작성하기 전까지 아무런 문제를 인지하지 못했고
gradle 빌드에서도 정상적으로 동작해왔다.
하지만 앞에서 언급한 에러를 해결하고 나서
다시 테스트 러너를 돌리자 아래와 같은 에러로그를 뱉었다.
Schema-validation:
wrong column type encountered in column [url] in table [video];
found [character (Types#CLOB)], but expecting [text (Types#VARCHAR)]
아래와 같이 Video 테이블이 생성되는 마이그레이션 파일에는
컬럼 생성에 대해 정의가 잘 되어있었다.
<column name="url" type="TEXT"/>
근데 왜 에러가 발생한 걸까?
Expecting: text (Types#VARCHAR)
Found: character (Types#CLOB)
이거는 뭔 소릴까?
검색을 해본결과 해결방안은 이것이었다.
수정 (2023.01.17)
@Lob 어노테이션을 붙인다
이 설명은 틀릴 수 있으나 나름대로 정리해본 걸 써보고자 한다.
@Lob를 붙이지 않으면 hibernate는 mapping을 형성할 때
TEXT와 VARCHAR를 구분하지 않게 된다.
즉, expected 컬럼타입이 VARCHAR로 잡히게 되고
실제 db에는 TEXT로 생성되어있기 때문에 타입충돌이 생기는 것.
위와 같이 해결후 Controller Test를 작성중 정말 이상한 에러를 마주했다
Error: Could not extract column [7] from JDBC ResultSet
[Bad value for type long : ] [n/a]; SQL [n/a]
파악 결과 @Lob의 문제인 것으로 나타났다
결국 이 모든 것은 RepositoryTest를 통과시키고자 시작한 일인데
그러면 해결방법은 어떻게 했느냐?
솔직히 아직까지도 정리가 잘 안됐고 해결만 한 상태이다.
궁극적으로 ddl-auto: update 로 설정해야 모든게 잘 돌아갔다.
dev용 프로파일은 validate로 유지하고 싶어서 test용 프로파일을 따로 분리했고,
분리하는 김에 db는 h2를 사용하도록 해서 테스팅과 분리시켰다.
넘버링한 프로세스는 아래와 같다.
- H2 를 사용한 test용 프로파일 생성
- 모든 Repository 테스트마다 @ActiveProfiles("test") 를 붙임
- ddl-auto: update 로 설정
# application-test.yml
spring:
datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
username: sa
password:
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: update
show-sql: true
h2:
console:
enabled: true
liquibase:
change-log: classpath:/db/changelog/db.changelog-master.xml
기존의 repository 테스트들은 dev 프로파일을 공유하고 있었다.
즉 로컬에서 띄운 docker postgresql 컨테이너를 공유중이었던 것.
문제가 없다고 생각한게 어차피 클래스 어노테이션에
@Transactional을 붙여서 테스트를 진행하고 있었기 때문이다.
Repository 테스트마다 모든게 롤백되어서
사실상 dev환경에서는 개발시 아무 지장이 없었기 때문에
테스트와 관련된 쪽으로는 고려를 하지 못한 것 같다.
솔직히 내가봐도 그냥 원인분석이 제대로 안되어있고
이것저것 때려맞추면서 해결한 거 같아
사실상 일기장 용도의 처참한 포스팅이 된 것 같다.
예상하건데 또 와서 수정할듯한 느낌이 든다 😇
마무리
- Liquibase 사용시 jpa-buddy diff가 제대로 create-table을 생성하지 않는 문제
- 단, 테스트에서만 문제가 생기고 gradle 빌드에서는 괜찮음
- @Lob 어노테이션을 붙이지 않아서 생기는 문제
위 2가지에 대한 해결방안을 정리해봤다.