이슈
rewriteBatchedStatements=true
고객사에서 위와 같은 mysql 옵션을 추가하자 다음과 같은 오류가 발생하였다.
java.lang.NullPointerException
at com.mysql.cj.ClientPreparedQuery.computeMaxParameterSetSizeAndBatchSize(ClientPreparedQuery.java:66)
오류가 발생한 코드는 Batch JDBC 코드를 작성할 때 자주 사용하는 보일러 플레이트 코드로 executeBatch()가 실행될 때 오류가 발생하였다.
Blob blob = ...
try (Connection connection = DBUtil.getInstance().getConnection();
PreparedStatement preparedStatement = connection.prepareStatement(query)) {
connection.setAutoCommit(false);
for (TransactionBatch batch : batches) {
try {
preparedStatement.setString(1, batch.getDeviceID());
preparedStatement.setBlob(2, blob);
preparedStatement.addBatch();
} catch (Exception e) {
e.printStackTrace();
}
}
preparedStatement.executeBatch();
blob.free();
해결
연구소에서 재현을 위해 rewriteBatchedStatements=true 옵션을 추가했는데 동일한 현상이 발생하지 않았고 추후 확인해보니 고객사와 mysql-connector-java 버전에 차이가 있었다.
연구소는 버전이 5.1.38, 고객사는 8.0.19였다. 버전을 고객사와 동일하게 맞춘 후 동일한 현상이 재현되었다.
구글링을 해보니 setBlob() 또는 setBinaryStream()을 사용할 때 문제가 발생하며 해당 메서드 대신 setBytes()를 사용해야 했다. setBytes()를 사용하면 오류가 발생하지 않는다.
이유
이유를 좀 더 찾아보자.
2015년 6월에 스택오버플로우에서 해당 관련 글이 올라왔다. (computeMaxParameterSetSizeAndBatchSize가 PreparedStatement에 있는 것으로 보아 버전이 달라보인다.). 해당 답변에서는 rewrite를 하기 위해 사이즈를 재 계산해야 하지만 input stream에서는 할 수 없다라는 의미처럼 보이지만, 정확히 이해가 되지 않는다.
2017년 3월에 mysql 커뮤니티에 버그 리포트(#85317)로 해당 관련 글이 올라왔다. mysql 개발자가 해당 글을 당일 확인하고 버그로 판단하고 해결되기까지...........무려 5년이 걸렸다.
5년 뒤인 8.0.29 버전에 이슈가 패치되었다고 한다. => 링크
- When the connection property rewriteBatchedStatements was set to true, inserting a BLOB using a prepared statement and executeBatch() resulted in a NullPointerException. (Bug #85317, Bug #25672958)
setBlob()을 사용한 기존 코드를 유지한 채 8.0.29 버전으로 수행하니 더 이상 오류가 발생하지 않았다.
8.0.19와 8.0.29로 코드를 비교해서 어떠한 점이 패치되었는지 판단하려 하였으나, 많은 부분이 변경 되어 확인하기가 어려웠다.
8.0.19 버전에서 NPE가 발생한 소스는 다음과 같다.
@Override
protected long[] computeMaxParameterSetSizeAndBatchSize(int numBatchedArgs) {
long sizeOfEntireBatch = 0;
long maxSizeOfParameterSet = 0;
for (int i = 0; i < numBatchedArgs; i++) {
ClientPreparedQueryBindings qBindings = (ClientPreparedQueryBindings) this.batchedArgs.get(i);
BindValue[] bindValues = qBindings.getBindValues();
long sizeOfParameterSet = 0;
for (int j = 0; j < bindValues.length; j++) {
if (!bindValues[j].isNull()) {
if (bindValues[j].isStream()) {
long streamLength = bindValues[j].getStreamLength();
if (streamLength != -1) {
sizeOfParameterSet += streamLength * 2;
} else {
int paramLength = qBindings.getBindValues()[j].getByteValue().length; // NPE 발생
sizeOfParameterSet += paramLength;
}
....
getByteValue()를 하는 중 배열에 들어 있는 값이 null이라 발생하지 않았을 까 싶다..