Liquibase의 table not found 에러와 @Lob

서론

이전에 기본적인 api를 개발할때 분명히 postman으로 잘 동작했던 코드가
클라이언트 사이드에서 동작하지 않는 것을 발견했다.

이건 말이 안되는 상황이라고 생각했고
이번에 개발하는 api에 대해서는

별로 테스트를 작성하고
클라이언트 작업으로 넘어가야겠다고 마음을 먹었다.

기본적인 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)

어이가 없었다.

category 테이블은 제대로 생성되어있었다.

그러면 인메모리에서 h2로 테스트를 돌릴 때도
당연히 생성해서 진행돼야 하는 거 아닌가?

한편, 에러로그는 정확한 것이 실제로 위 마이그레이션 파일에는

<createTable tableName="category">
...
</createTable>

이 부분이 정말로 빠져있었다.

이 지점에 대한 의문점을 해소하지 못했다.
의문점을 해소하지 못했기 때문에 글로 남기는 것이기도 하다.

해결한 방법

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

문서에서는 여러 방법을 소개한다

  1. Database Changelog
  2. validCheckSum
  3. clear-checksums

이 중에서 1번을 택했다.
마이그레이션 파일의 validChecksum을 계속 추가하면서
가독성을 떨어뜨리고 싶지 않았기 때문이다.

문제는 당연히 존재한다.

예상이지만 현재 RDS에 올라가있는 db에서도
첫번째 build를 할 경우
jdbc가 연결될 때 이와 동일하게 crash가 발생할 거라고 본다.

dev환경에서 위와 같이 해결했기 때문에
datagrip으로 똑같이 해결해 줘야 할 것이다.
이 과정에서 휴먼에러가 발생할 수 있다.

이런 지점에서 생각해보면 3번이 제일 괜찮을 수 있다는 생각이 든다.
현재로서는 모든 걸 지우고 다시 체크섬을 부여했을 때
발생할 수 있는 side-effect가 떠오르지 않는다.

1번을 택했다면 해결방식은 간단하다.

  1. intellij database > database_changelog 테이블에 진입
  2. 문제가 되는 old checksum을 locate
  3. validation failed 로그에서 보여준 새로운 checksum으로 교체
  4. 변경사항을 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를 사용하도록 해서 테스팅과 분리시켰다.
넘버링한 프로세스는 아래와 같다.

  1. H2 를 사용한 test용 프로파일 생성
  2. 모든 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환경에서는 개발시 아무 지장이 없었기 때문에
테스트와 관련된 쪽으로는 고려를 하지 못한 것 같다.

솔직히 내가봐도 그냥 원인분석이 제대로 안되어있고
이것저것 때려맞추면서 해결한 거 같아
사실상 일기장 용도의 처참한 포스팅이 된 것 같다.
예상하건데 또 와서 수정할듯한 느낌이 든다 😇


마무리

위 2가지에 대한 해결방안을 정리해봤다.