JdbcTemplate의 정의

템플릿 콜백 패턴을 사용하여 JDBC를 직접 사용할 때 발생하는 반복 작업을 대신 처리해주는 유틸리티

JDBC 단순 사용과의 비교

[Before]

@RequiredArgsConstructor
public class BeforeRepository {  
	
	private final DataSource dataSource;
	private final SQLExceptionTranslator exTranslator;
	
	public void update(String memberId, int money) { 
	    String sql = "update MEMBER set money=? where member_id=?";  
		
		Connection con = null;
	    PreparedStatement pstmt = null;  
		
	    try {  
		    con = DataSourceUtils.getConnection(datasource);
	        pstmt = con.prepareStatement(sql);  
	        pstmt.setInt(1, money);  
	        pstmt.setString(2, memberId);  
	        pstmt.executeUpdate();  
	    } catch (SQLException e) {  
	        throw exTranslator.translate("update", sql, e);
	    } finally {  
	        JdbcUtils.closeStatement(rs);
	        JdbcUtils.closeStatement(pstmt); 
	        DataSourceUtils.releaseConnection(con, dataSource);
	    }  
	}
}

[After]

public class AfterRepository {  
	
	private final JdbcTemplate template;
	
	public AfterRepository(DataSource dataSource) {
		template = new JdbcTemplate(dataSource);
	}
	
	public void update(String memberId, int money) { 
	    String sql = "update MEMBER set money=? where member_id=?";  
		template.update(sql, money, memberId);
	}
}

처리해주는 반복 작업 목록

  • 커넥션 획득
  • statement 를 준비하고 실행
  • 결과를 반복하도록 루프를 실행
  • 커넥션 종료, statement , resultset 종료
  • 트랜잭션 다루기 위한 커넥션 동기화
  • 예외 발생시 스프링 예외 변환기 실행
  • 등등

이름을 지정하여 파라미터 바인딩하기

String sql = "update MEMBER set money=? where member_id=?";  
template.update(sql, money, memberId);

해당 부분을 보면 파라미터 ?가 있는 순서대로 들어갈 값을 update()의 파라미터로 넣었습니다.
하지만 이는 개발자가 직접 순서를 확인하며 넣는 것이기에 실수의 위험성이 커 SQL에서 ?가 있는 부분에 이름을 지정하여 파라미터를 바인딩할 수 있습니다.

우선 JdbcTemplate대신 NamedParameterJdbcTemplate를 주입받도록 해야 합니다.
NamedParameterJdbcTemplate또한 DataSource를 주입받습니다.

public class Repository {  
	private final NamedParameterJdbcTemplate template;
	
	public Repository(DataSource dataSource) {
		this.template = new NamedParameterJdbcTemplate(dataSource);
	}
	
	...
}

SQL의 파라미터 이름 지정은 아래와같이 SQL에서 ?대신 :이름을 넣으면 됩니다.

String sql = "update MEMBER set money=:money where member_id=:id";  

해당 파라미터에 들어갈 값을 매칭해줄 데이터를 생성하는 방법은 3가지가 있습니다.

1. Map 사용

java의 기본 기능인 Map을 사용하여 SQL에 넣은 파라미터의 이름과 해당 파라미터에 들어갈 값 쌍을 구성할 수 있습니다.

public class Repository {  
	
	private final NamedParameterJdbcTemplate template;
	
	public Repository(DataSource dataSource) {
		this.template = new NamedParameterJdbcTemplate(dataSource);
	}
	
	public void update(String memberId, int money) { 
	    String sql = "update MEMBER set money=:money where member_id=:id";
	    
	    Map<String, Object> param = Map.ofEntries(
			Map.entry("id", 1),
			Map.entry("money", 10000)
		);
		
		template.update(sql, param);
	}
}

2. MapSqlParameterSource 사용

Map과 유사하지만 SQL 타입을 지정할 수 있는 등 SQL에 더 특화된 기능을 제공합니다. SqlParameterSource인터페이스의 구현체이고, Map보단 사용이 편리합니다.

public class Repository {  
	
	private final NamedParameterJdbcTemplate template;
	
	public Repository(DataSource dataSource) {
		this.template = new NamedParameterJdbcTemplate(dataSource);
	}
	
	public void update(String memberId, int money) { 
	    String sql = "update MEMBER set money=:money where member_id=:id";
	    
	    SqlParameterSource param = new MapSqlParameterSource()
		    .addValue("id", memberId)
		    .addValue("money", money);
		
		template.update(sql, param);
	}
}

3. BeanPropertySqlParameterSource 사용

SQL 쿼리에 바인딩할 파라미터를 객체의 필드로부터 자동으로 매핑해주는 기능을 제공합니다.
SqlParameterSource인터페이스의 구현체입니다.

만일 아래와 같이 update를 위한 Dto가 있다고 가정해보겠습니다.

@Getter
public class UpdateMemberDto {
	
	private String id;
	private int money;
}

그럼 UpdateMemberDto객체로부터 SQL 쿼리에 매핑될 데이터를 BeanPropertySqlParameterSource를 통해 구성할 수 있습니다.

public class Repository {  
	
	private final NamedParameterJdbcTemplate template;
	
	public Repository(DataSource dataSource) {
		this.template = new NamedParameterJdbcTemplate(dataSource);
	}
	
	public void update(UpdateMemberDto updateMemberDto) { 
	    String sql = "update MEMBER set money=:money where member_id=:id";
	    
	    SqlParameterSource param = new BeanPropertySqlParameterSource(updateMemberDto);
		
		template.update(sql, param);
	}
}

기본키 자동 지정 상황 대응하기

기본키가 자동으로 지정되는 테이블에선, 해당 기본키를 데이터가 추가 되어야지만 확인할 수 있습니다.

create table member
(
	member_id bigint generated by default as identity,
	money integer,
	primary key (id)
);

만일 위와같이 id값이 자동으로 지정되는 테이블인 상황에서 member 데이터의 생성은 어떻게 해야 할까요?

1. KeyHolder 사용

KeyHolder 타입의 변수를 JdbcTemplate에서 update()를 진행할 때 마지막 파라미터로 넣어주면 DB에 데이터를 생성한 후 지정된 기본키 값을 가져올 수 있습니다.

public class Repository {  
	
	private final NamedParameterJdbcTemplate template;
	
	public Repository(DataSource dataSource) {
		template = new NamedParameterJdbcTemplate(dataSource);
	}
	
	public Member save(Member member) { 
	    String sql = "insert into member (money) values (:money)";
	    KeyHolder keyHolder = new GeneratedKeyHolder();
	    SqlParameterSource param = new BeanPropertySqlParameterSource(member);
		
		template.update(sql, param, keyHolder);
		
		long key = keyHolder.getKey().longValue();
		member.setMemberId(key);
		return member;
	}
}

2. SimpleJdbcInsert 사용

JdbcTemplate는 insert하는 과정을 편리하게 해주는 SimpleJdbcInsert라는 기능을 제공합니다.

2-1. 생성자 적용

우선 SimpleJdbcInsert를 주입받아야 합니다.

public class Repository {  
	
	private final NamedParameterJdbcTemplate template;
	private final SimpleJdbcInsert jdbcInsert;
	
	public Repository(DataSource dataSource) {
		this.template = new NamedParameterJdbcTemplate(dataSource);
		this.jdbcInsert = new SimpleJdbcInsert(dataSource)
			.withTableName("member")
			.usingGeneratedKeyColumns("member_id");
	}
	
}

위와 같이 선언할 때 withTableName()을 통해 해당 테이블 명을 지정하고, usingGeneratedKeyColumns()을 총해 기본키 이름을 명시해줘야 합니다.

2-2. 사용

executeAndReturnKey()을 사용하여 쿼리 작성도 필요 없이 간단하게 insert를 진행하고, 완료 후 생성된 key값을 받아올 수 있습니다.

public class Repository {  
	
	private final NamedParameterJdbcTemplate template;
	private final SimpleJdbcInsert jdbcInsert;
	
	public Repository(DataSource dataSource) {
		this.template = new NamedParameterJdbcTemplate(dataSource);
		this.jdbcInsert = new SimpleJdbcInsert(dataSource)
			.withTableName("member")
			.usingGeneratedKeyColumns("member_id");
	}
	
	public Member save(Member member) { 
	    SqlParameterSource param = new BeanPropertySqlParameterSource(member);
		Number key = jdbcInsert.executeAndReturnKey(param);
		item.setId(key.longValue());
		return item;
	}
}

JdbcTemplate의 단점

예를들어 조회 서비스를 만드는데 money에 값이 없으면 그냥 모든 member를 보여주고, money에 값이 있으면 해당 money보다 더 많이 갖고있는 사용자를 보여준다고 할 때, money에 값이 없는 경우의 쿼리는 아래와 같습니다.

SELECT * FROM MEMBER

그리고, money에 값이 있는 경우의 쿼리는 아래와 같습니다.

SELECT * FROM MEMBER WHERE MONEY > :money

즉 코드 내에서 조건에 따라 SQL 쿼리문이 동적으로 변해야 하는 경우도 있는데, JdbcTemplate는 따로 해당 동적 변환 기능이 없기 때문에 쿼리 문자열에서 조건을 더해 WHERE MONEY > :money를 money가 있을 때 더하는 식으로 무식하게 처리할 수 밖에 없습니다.

이런 동적 쿼리 문제를 해결하기 위해선 MyBatis를 적용해야 합니다.