๐ซ ๋ฐฐ๊ฒฝ
์งํ ์ค์ธ ํ๋ก์ ํธ์ ๊ณผ๊ฑฐ ์ฝ๋ ์ค 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 ์ค ์ด๋ค ํ์ ์ด ๋ง์์ง ์ ๊ณ ๋ฏผํด์ ์ ํด์ผ ํ๊ณ , ๋ฐ์ดํฐ๋ฒ ์ด์ค ํ์ ๊ณผ ํ๋ ํ์ ์ ๋ฐ๋์ ๋ง์ถฐ์ผ ํ๋ค.
๋๊ธ