query dsl 을 사용해서 insert 와 update 기능을 구현했는데 한 꺼번에 수많은 데이터를 집어넣어야 하는 경우가 발생해서 성능적인 부분에서 이슈가 발생했다.
spring jpa 를 사용해서 batch insert로 데이터를 집어 넣을 수 있지만,
@GeneratedValue(strategy = GenerationType.IDENTITY) 를 사용하고 있을 경우 db에 insert 수행이후 id 값을 알 수 있기 때문에 쓰기 지연 기능이 불가, batch insert 사용이 불가능 하다. 따라서 jdbc template을 이용해서 직접 쿼리를 날려줘서 batch insert를 수행해야 한다.
굳이 jpa 를 이용하여 수행할 수 있는 방법이 있지만 key 매핑 전략을 변경해야 하고 어떤 db 를 사용하냐에 따라서 사용가능한 매핑 전략이 제한되기 때문에 그런 수고스러운 과정을 진행할 바에 간편하게 JdbcTemplate 을 이용하여 batch insert 를 수행하는게 더 효율 적이라고 생각한다.
템플릿을 직접 이용해야 할 경우 직접 쿼리를 날려줘야 하기 때문에 사용 방법과 sql 쿼리를 작성할 수 있어야 한다.
bulk 연산을 위해서 JdbcTemplate를 사용할 경우 단건의 crud 가 아니기 때문에 List<Object> 형태의 값이 주 타겟이다. 당연히 단건의 crud일 경우에도 사용할 수 있지만 단건이라면 굳이 template을 직접 이용할 필요는 없다. 표준화된 형식의 insert, update, delete 의 경우 batchUpdate() 함수를 이용하여 모두 처리할 수 있다.
업데이트 과정을 보기 위해 Bulk 라는 클래스를 만들어 줬다.
public class Bulk {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String a;
private String b;
private boolean c;
private int d;
private Map<String, String> map;
public Bulk(String a, String b, boolean c, int d, Map<String, String> map){
this.a = a;
this.b = b;
this.c = c;
this.d = d;
this.map = map;
}
}
실무에서 벌크 연산을 통해서 넣는 것은 사실 별로 어려운 것이 아닌지만 약간 까다롭게 처리해야 할 부분은 있다.
예를 들어서 일반적인 Object 형식 같은 경우에는 전 처리 과정을 통해서 넣을 수 있지만 독특한 형태의 map 과 같은 Object 의 경우 전처리 과정을 통해서 변환을 해 준다음에 넣어야 한다. 가장 기본적인 형태로 JSON 타입의 직렬화 상태를 통해서 String 값으로 변환해서 넣어주면 되는데 아래와 같은 전 처리 방식을 통해서 bulk insert를 진행할 수 있다.
public int[] bulkInsert(List<Bulk> bulks){
String sql = "insert into bulk (a, b, c, d, map)" + " values (?, ?, ?, ?, ?)";
return jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
Bulk bulk = bulks.get(i);
ps.setString(1, bulk.getA());
ps.setString(2, bulk.getB());
ps.setBoolean(3, bulk.isC());
ps.setInt(4, bulk.getD());
// map 변환해서 넣는 코드 필요.
StringBuilder mapAsString = new StringBuilder("{");
if (bulk.getMap() != null && !bulk.getMap().isEmpty()) {
for (String key : bulk.getMap().keySet()) {
mapAsString
.append("\"")
.append(key)
.append("\":\"")
.append(bulk.getMap().get(key))
.append("\", ");
}
mapAsString.delete(mapAsString.length() - 2, mapAsString.length());
}
mapAsString.append("}");
StringReader reader = new StringReader(mapAsString.toString());
}
@Override
public int getBatchSize() {
return bulks.size();
}
});
}
실제로 테이블에 Map 이 아닌 다른 형태의 Object, 예를 들어 List 형태의 컬럼이나 혹은 Set 형태의 컬림이 값으로 들어 갈 경우 StringBuilder 를 통해 직접 값을 변환 시키는 전 처리 과정을 진행 한다면 데이터를 넣거나 업데이트 하는 것에 크게 어려움이 들지는 않을 것이다.
bulkUpdate 도 bulkInsert 와 크게 차이는 없어서 같은 함수를 통해서 구현을 할 수가 있었다.
public int[] bulkUpdate(List<Bulk> bulks){
String sql = "update bulk set c=?, d=?"+" where ?=?";
return jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
Bulk bulk = bulks.get(i);
ps.setBoolean(1, bulk.isC());
ps.setInt(2, bulk.getD());
ps.setString(3, bulk.getA());
ps.setString(4, bulk.getB());
}
@Override
public int getBatchSize() {
return bulks.size();
}
});
}
마지막으로 사실 JdbcTemplate 을 이용해서만 벌크 쿼리를 작성할 수 있는것은 아니다. jdbc를 이용해서 sql을 넘길 수 있는 클래스는 여러가지고 특정 클래스의 메소드 사용 방법에 따라서 sql 문이 변경 될 수 있다. 예를 들어서 NamedParameterJdbcTemplate 같은 경우에는 많은 value 값을 sql로 작성해야 할 경우 물음표 를 통해서 쿼리를 작성하다 보면 개발자의 실수가 발생 할 수 있는데 그 부분을 보완하여 특정 지정값을 통해 쿼리작성의 오류를 줄일 수 있다. 또한 다양한 메소드를 통해서 쿼리를 작성할 수 있기 때문에 어떠한 전처리 과정을 거치느냐에 따라서 같은 jdcbTemplate 을 사용하더라도 호출 메소드에 따라 크지않은 성능의 차이가 발생할 수 있다.