spring / / 2023. 9. 8. 07:37

spring data jpa를 사용할 때 postgresql의 @Lob타입 문제

spring data jpa를 사용하여 DB를 연결할 때 DB 종류가 postgresql인 경우에 @Lob 유형 사용 시 성능 문제를 마주치게 된다. 여기서는 postgresql의 @Lob 유형 사용 시 발생하는 속도 저하 문제의 현상과 개선 방법에 대한 내용과 모든 DB에서 사용할 수 있도록 postgresql용 dialect만 수정하는 방법에 대해 설명해 보고자 한다.

제약사항: 현재 사용하려는 데이터베이스가 postgresql로 한정하지 않고 여러 데이터베이스를 지원해야 하므로 특정 버전의 DB를 위한 어노테이션은 사용하면 안된다.

시나리오

우선, 문제를 재현하기 위한 기본적인 사용 시나리오를 만들어보자. 사용자 메시지를 100건을 등록하고 해당 메시지를 조회를 하여 얼마나 오래 걸리는지 측정했다.

  • 사용자 메시지를 100건 등록 (@Lob)
  • 100건의 메시지 조회

소스코드

jpa

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class UserJpo {

    @Id
    private long id;

    @Lob
    private String message;
}

테스트 코드

@SpringBootTest
@Slf4j
public class UserRepositoryTest {

    @Autowired
    UserRepository userRepository;

    int count = 100;

    void createMessage() {
        String message = "사용자 메시지 ";
        IntStream.range(1, count)
            .forEach(i -> userRepository.save(userRepository.save(new UserJpo(i, message + i))));
    }

    @Test
    public void find() {
        createMessage();

        long start = System.currentTimeMillis();
        List<UserJpo> userJpos = userRepository.findAll();
        log.info("total count : {}", userJpos.size());
        long elapsed = System.currentTimeMillis() - start;
        log.info("elapsed : {}(ms)", elapsed);
    }
}

application.yml

spring:
  jpa:
    database-platform: org.hibernate.dialect.PostgreSQL10Dialect
    hibernate:
      ddl-auto: create
    show-sql: false
  datasource:
    url: jdbc:postgresql://[URL]:5432/[database]
    username: [user]
    password: [password]
    driverClassName: org.postgresql.Driver

logging:
  level:
    org.hibernate.SQL: debug
    org.hibernate.type.descriptor.sql: info

실행결과

2023-09-08 06:40:44.965 DEBUG 11366 --- [           main] org.hibernate.SQL                        : select userjpo0_.id as id1_0_, userjpo0_.message as message2_0_ from user_jpo userjpo0_
2023-09-08 06:40:48.384  INFO 11366 --- [           main] c.e.user.store.jpa.UserRepositoryTest    : total count : 99
2023-09-08 06:40:48.385  INFO 11366 --- [           main] c.e.user.store.jpa.UserRepositoryTest    : elapsed : 3501(ms)

여기서 100건의 데이터를 조회하는 데 3초 이상 걸린다. 100건에 3초면 뭔가 문제가 있다는 것을 직감할 수 있다.

데이터 타입 확인

@Lob으로 지정한 필드가 postgresql에서 어떤 타입으로 생성되는지 확인하기 위해 hibernate SQL로그를 출력했다.

SQL을 출력하기 위해 application.yml에 아래 코드를 추가한다.

spring:
  jpa:
    show-sql: false

logging:
  level:
    org.hibernate.SQL: debug
    org.hibernate.type.descriptor.sql: info

테스트케이스를 다시 실행하여 SQL을 확인해보니 oid 타입으로 지정이 된다.

create table user_jpo (id int8 not null, message oid, primary key (id));

TEXT나 CLOB으로 생성되기를 원했는데 oid 타입으로 생성되었다.

oid는 기본적인 RDBMS에서는 없는 특별한 타입인 것 같은데, oid타입은 postgresql에서 내부 객체들을 구분하기 위해 만든 특별한 타입이라고 한다. (Object Identifier)

그럼 이런 문제를 해결하기 위한 방법들을 알아보자.

해결방법

방법1) @Column(columnDefinition = "text") 추가

@Lob
@Column(columnDefinition = "text")
private String message;

@Column 어노테이션으로 columnDefinition을 정의하면 테이블 생성 시 text 필드로 생성이 된다.

위와 같이 @Column(columnDefinition = "text")를 추가하고 실행해보고 SQL을 확인해보자.

create table user_jpo (id int8 not null, message text, primary key (id))

text 타입으로 잘 생성되는 것을 확인할 수 있다.

그리고 100건을 조회하는 속도는 87(ms)정도 걸린다. 잘 해결된 것으로 보인다.

하지만 postgresql이 아닌 오라클을 사용하는 경우 아래와 같이 스크립트가 만들어지는데, 오라클은 text 유형이 없어서 실행할 수 없어 테이블 생성하다가 오류가 발생한다. (오라클은 long이나 clob을 사용해야 한다)

create table user_jpo (id number(19,0) not null, message text, primary key (id))

방법2) @Type(type = "org.hibernate.type.TextType") 추가

이렇게 하니 postgresql에서 text타입으로 잘 생성이 된다.

오라클의 경우도 잘 생성이 된다. 하지만 우리가 예상한 clob으로 생성이 되지 않고 long 타입으로 생성이 된다.

create table user_jpo (id number(19,0) not null, message long, primary key (id)) // oracle

오라클에서 TextType은 long타입으로 매핑이 되어 있는 것 같다.

오라클에서 long 타입은 여러 문제가 있어서 가능한 사용하지 않는 것으로 알고 있다.

방법3) Custom Postgresql Dialect 수정

postgresql에서 @Lob으로 지정한 경우에 oid가 아닌 text로 생성하게 할 수 없을까 생각되어 hibernate 소스코드를 찾아봤다.

PostgreSQL81Dialect.java 코드를 보니 hibernate 타입을 DB 필드로 매핑하는 로직이 있었다.

public PostgreSQL81Dialect() {
    super();
    registerColumnType( Types.TIME, "time" );
    registerColumnType( Types.TIMESTAMP, "timestamp" );
    registerColumnType( Types.VARBINARY, "bytea" );
    registerColumnType( Types.BINARY, "bytea" );
    registerColumnType( Types.LONGVARCHAR, "text" );
    registerColumnType( Types.LONGVARBINARY, "bytea" );
    registerColumnType( Types.CLOB, "oid" );
    registerColumnType( Types.BLOB, "oid" );
    ...
}

그래서 Dialect를 재정의하고 타입을 변경하면 될 듯 하여 CustomPostgresqlDialect를 만들고 CLOB을 TEXT타입으로 매핑했다.

@Slf4j
public class CustomPostgresqlDialect extends PostgreSQL10Dialect {

    public CustomPostgresqlDialect() {
        super();
        registerColumnType(Types.CLOB, StandardBasicTypes.TEXT.getName()); // 추가
    }
}

생성되는 스크립트를 보니 text로 잘 생성이 된다.

create table user_jpo (id int8 not null, message text, primary key (id))

이렇게 하고 다시 테스트 케이스를 돌려봤다.

하지만 실행속도는 똑같이 3초 이상 걸렸다.

2023-09-08 07:27:35.411  INFO 13477 --- [           main] c.e.user.store.jpa.UserRepositoryTest    : total count : 99
2023-09-08 07:27:35.411  INFO 13477 --- [           main] c.e.user.store.jpa.UserRepositoryTest    : elapsed : 3807(ms)

Dialect.java를 보니 @Clob으로 지정된 필드를 조회할 때 Stream방식으로 동작하게끔 되어 있었다.

protected SqlTypeDescriptor getSqlTypeDescriptorOverride(int sqlCode) {
  SqlTypeDescriptor descriptor;
  switch ( sqlCode ) {
    case Types.CLOB: {
      descriptor = useInputStreamToInsertBlob() ? ClobTypeDescriptor.STREAM_BINDING : null;
      break;
    }
    default: {
      descriptor = null;
      break;
    }
  }
  return descriptor;
}

그래서, 테이블 생성 시에도 변경을 해야 하지만 조회할 때도 @Lob 필드를 LongVarchar로 조회하게 변경해야 한다.

아래와 같이 코드를 추가했다.

@Slf4j
public class CustomPostgresqlDialect extends PostgreSQL10Dialect {

    public CustomPostgresqlDialect() {
        super();
        registerColumnType(Types.CLOB, StandardBasicTypes.TEXT.getName());
    }


    @Override
    public SqlTypeDescriptor getSqlTypeDescriptorOverride(int sqlCode) {
        if (sqlCode == Types.CLOB) {
            return LongVarcharTypeDescriptor.INSTANCE; // 추가
        } else {
            return super.getSqlTypeDescriptorOverride(sqlCode);
        }
    }
}

그리고 spring에서 postgresql을 사용할 때 dialect를 지정을 기본 PostgreSQL10Dialect이 아닌 위에서 만든 CustomPostgresqlDialect으로 변경했다.

spring:
  jpa:
    database-platform: com.example.lib.CustomPostgresqlDialect
#    database-platform: org.hibernate.dialect.PostgreSQL10Dialect

실행해보니 정상적으로 잘 동작하였다.

2023-09-08 07:33:10.824  INFO 13637 --- [           main] c.e.user.store.jpa.UserRepositoryTest    : total count : 99
2023-09-08 07:33:10.824  INFO 13637 --- [           main] c.e.user.store.jpa.UserRepositoryTest    : elapsed : 67(ms)

실행속도도 빨라졌다.

결론

특정 DB에 특화된 기능을 수정할 때는 dialect를 재구현하여 작업을 진행할 수 있었다. 위에서 제공한 메소드 외에 다양하게 재구현할 수 있는 메소드가 많으니 필요할 때 사용할 수 있을 듯 하다.

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유