Spring/JPA

@Lob μ‚¬μš©μ‹œ DB와 μ—”ν‹°ν‹° ν•„λ“œ νƒ€μž…μ΄ λ‹€λ₯Έ 경우 λ°œμƒν•˜λŠ” λ¬Έμ œμ™€ 해결방법

ν‘μ‹œλ°” 2024. 1. 7. 20:16

😫 배경

μ§„ν–‰ 쀑인 ν”„λ‘œμ νŠΈμ˜ κ³Όκ±° μ½”λ“œ 쀑 HTML λ¬Έμ„œλ₯Ό κ·ΈλŒ€λ‘œ DB에 μ €μž₯ν•˜λŠ” λ‚΄μš©μ΄ μžˆμ—ˆλ‹€. HTML λ¬Έμ„œ λ‚΄μš©μ€ 동적이고, 길이가 κΈΈμ—ˆκΈ° λ•Œλ¬Έμ— κΈ΄ λ¬Έμžμ—΄ μ €μž₯을 μœ„ν•΄ LOB을 μ‚¬μš©ν–ˆκ³ , 별닀λ₯Έ λ¬Έμ œμ—†μ΄ μ„œλΉ„μŠ€λ₯Ό μ΄μš©ν•΄ μ™”μ—ˆλ‹€. κ·ΈλŸ¬λ‹€ μ–΄λŠ μˆœκ°„ μ„œλΉ„μŠ€λ₯Ό μ΄μš©ν•  수 μ—†λŠ” λ¬Έμ œκ°€ λ°œμƒν–ˆλ‹€. 이와 κ΄€λ ¨ν•΄μ„œ νŒŒμ•…ν•œ 원인과 ν•΄κ²° 방법에 λŒ€ν•΄μ„œ κ³΅μœ ν•˜κ³ μž ν•œλ‹€.

πŸ”Ž 원인

문제 λ‚΄μš©μ„ 뢄석해 λ³΄λ‹ˆ, 'DB에 μ„ μ–Έλœ LOB νƒ€μž…'κ³Ό '엔티티에 μ‚¬μš©λœ @LOB νƒ€μž…'이 λ‹€λ₯΄κ²Œ μ‚¬μš©λ˜μ—ˆκΈ° λ•Œλ¬Έμ— λ°œμƒν•œ λ¬Έμ œμ˜€λ‹€.

 

μ„œλΉ„μŠ€ DBμ—μ„œλŠ” BLOB νƒ€μž…μ„ μ μš©μ‹œμΌ°λŠ”λ°, 막상 μ—”ν‹°ν‹° ν•„λ“œμ—μ„œλŠ” String νƒ€μž…μœΌλ‘œ μ„ μ–Έν–ˆκΈ° λ•Œλ¬Έμ— CLOB κ΄€λ ¨ 둜직이 μ μš©λ˜λ©΄μ„œ λ°μ΄ν„°λ² μ΄μŠ€μ—μ„œ κ°€μ Έμ˜¨ 데이터λ₯Ό μ²˜λ¦¬ν•  수 μ—†κ²Œ λ˜μ–΄ λ¬Έμ œκ°€ λ°œμƒν•œ 것이닀.

 

@Lob μ‚¬μš© μ‹œ μŠ€ν”„λ§μ€ ν•„λ“œ νƒ€μž…μ— 따라 μžλ™μœΌλ‘œ κ΄€λ ¨ jdbc 객체둜 μ—°κ²°ν•΄ μ£ΌλŠ”λ°, 컴파일 λ‹¨κ³„μ—μ„œλŠ” 이λ₯Ό 확인할 수 μ—†μœΌλ―€λ‘œ ν•΄λ‹Ή 문제λ₯Ό νŒŒμ•…ν•  수 μ—†μ—ˆλ‹€.

λ°œμƒν•œ 문제

1. Invalid UTF8  

CLOB은 기본적으둜 UTF8 인코딩을 κ°€μ •ν•œλ‹€. κ·ΈλŸ¬λ―€λ‘œ 데이터λ₯Ό κ°€μ Έμ˜¬ λ•Œ 길이λ₯Ό κ³„μ‚°ν•˜λŠ” κ³Όμ •μ—μ„œ UTF-8을 μ μš©ν•΄μ„œ 길이λ₯Ό κ³„μ‚°ν•œλ‹€.

MariaDbClob.java

  public long length() {
    // The length of a character string is the number of UTF-16 units (not the number of characters)
    long len = 0;
    int pos = offset;

    // set ASCII (<= 127 chars)
    while (len < length && data[pos] > 0) {
      len++;
      pos++;
    }

    // multi-bytes UTF-8
    while (pos < offset + length) {
      byte firstByte = data[pos++];
      if (firstByte < 0) {
        if (firstByte >> 5 != -2 || (firstByte & 30) == 0) {
          if (firstByte >> 4 == -2) {
            if (pos + 1 < offset + length) {
              pos += 2;
              len++;
            } else {
              throw new UncheckedIOException("invalid UTF8", new CharacterCodingException());
            }
          } else if (firstByte >> 3 != -2) {
            throw new UncheckedIOException("invalid UTF8", new CharacterCodingException());
          } else if (pos + 2 < offset + length) {
            pos += 3;
            len += 2;
          } else {
            // bad truncated UTF8
            pos += offset + length;
            len += 1;
          }
        } else {
          pos++;
          len++;
        }
      } else {
        len++;
      }
    }
    return len;
  }

 

ν•΄λ‹Ή λΆ€λΆ„μ—μ„œ BLOB νƒ€μž… 데이터λ₯Ό 길이λ₯Ό κ³„μ‚°ν•˜λŠ” κ³Όμ • 쀑 λ¬Έμ œκ°€ λ°œμƒν•˜μ—¬ "invalid UTF8" λ©”μ‹œμ§€μ™€ ν•¨κ»˜ UncheckedIOException μ˜ˆμ™Έκ°€ λ°œμƒν•˜κ²Œ λœλ‹€.

 

MariaDbBlob.java 파일의 length() λ©”μ„œλ“œλŠ” 데이터 길이 κ·ΈλŒ€λ‘œλ₯Ό λ°˜ν™˜ν•œλ‹€.

 

  public long length() {
    return length;
  }

2. Data type BLOB cannot be decoded as Clob

ν•΄λ‹Ή λ¬Έμ œλŠ” 1번 문제λ₯Ό μž¬μ—°ν•˜λ €κ³  ν–ˆμ„ λ•Œ 쑰회 κ³Όμ • ν…ŒμŠ€νŠΈμ—μ„œ λ°œμƒν•œ μ˜ˆμ™Έμ΄λ‹€.

(μŠ€ν”„λ§ λΆ€νŠΈ 버전이 μ—…κ·Έλ ˆμ΄λ“œλ˜λ©΄μ„œ length 이전에 ν•„ν„°λ§λ˜λŠ” κ²ƒμœΌλ‘œ μΆ”μΈ‘λœλ‹€.)

ClobCodec.java

  @SuppressWarnings("fallthrough")
  private Clob getClob(ReadableByteBuf buf, int length, Column column) throws SQLDataException {
    switch (column.getType()) {
      case BLOB:
      case TINYBLOB:
      case MEDIUMBLOB:
      case LONGBLOB:
        if (column.isBinary()) {
          buf.skip(length);
          throw new SQLDataException(
              String.format("Data type %s cannot be decoded as Clob", column.getType()));
        }
        // expected fallthrough
        // BLOB is considered as String if it has a collation (this is TEXT column)

      case STRING:
      case VARCHAR:
      case VARSTRING:
        Clob clob = new MariaDbClob(buf.buf(), buf.pos(), length);
        buf.skip(length);
        return clob;

      default:
        buf.skip(length);
        throw new SQLDataException(
            String.format("Data type %s cannot be decoded as Clob", column.getType()));
    }
  }

 

column νƒ€μž…μ„ ν™•μΈν•΄μ„œ, Blob νƒ€μž…μΈ 경우 SQLDataException μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€.

쑰회 κ³Όμ •μ—μ„œ λ°œμƒν•˜λŠ” λ¬Έμ œμ΄λ―€λ‘œ, 데이터λ₯Ό μ €μž₯ν•˜κΈ°λ§Œ ν•œλ‹€λ©΄ λ¬Έμ œκ°€ λ°œμƒν•˜μ§€ μ•ŠλŠ”λ‹€. 

해결방법

HTML 문자λ₯Ό κ·ΈλŒ€λ‘œ μ €μž₯ν•˜λ‹€ λ³΄λ‹ˆ 데이터가 BLOB보닀 CLOB νƒ€μž…μ— μ–΄μšΈλ¦°λ‹€κ³  μƒκ°ν•΄μ„œ DB 데이터 μœ ν˜•μ„ CLOB νƒ€μž…μœΌλ‘œ μˆ˜μ •ν•˜λ €κ³  ν–ˆλ‹€. ν•˜μ§€λ§Œ λ³€κ²½ κ³Όμ •μ—μ„œ UTF8 λ¬Έμ œκ°€ λ°œμƒν•˜λ©° μˆ˜μ •μ— μ‹€νŒ¨ν–ˆλ‹€.

(이미 DB 내에 λ§Žμ€ 데이터가 μŒ“μ—¬ μžˆμ—ˆκ³ , μ €μž₯된 데이터 쀑 UTF8둜 인식할 수 μ—†λŠ” 데이터가 μ‘΄μž¬ν•˜μ˜€μŒ)

 

@Lob μ–΄λ…Έν…Œμ΄μ…˜μ„ μ œκ±°ν•˜λ©΄ Lob 처리 과정을 μƒλž΅ν•˜κΈ° λ•Œλ¬Έμ— ν•΄λ‹Ή 방법을 μ„ νƒν•΄μ„œ 문제λ₯Ό ν•΄κ²°ν–ˆλ‹€.

κ²°λ‘ 

@Lob을 μ‚¬μš©ν•˜κΈ° 전에 μ €μž₯될 데이터 μœ ν˜•μ΄ BLOB, CLOB 쀑 μ–΄λ–€ νƒ€μž…μ΄ λ§žμ„μ§€ 잘 κ³ λ―Όν•΄μ„œ μ •ν•΄μ•Ό ν•˜κ³ , λ°μ΄ν„°λ² μ΄μŠ€ νƒ€μž…κ³Ό ν•„λ“œ νƒ€μž…μ€ λ°˜λ“œμ‹œ λ§žμΆ°μ•Ό ν•œλ‹€.