JdbcTemplate에 관하여

슬링민키·2025년 4월 20일

Spring

목록 보기
1/3
post-thumbnail

Spring을 공부하게 되면서 데이터베이스에 연결시켜야하는 일이 생겼다. 이때 Spring 에서의 JDBC api의 활용을 도와주는 JdbcTemplate을 학습해보고자 한다.

또한 jdbcTemplate을 점점더 사용하면서 알게된 정보드를 추가적으로 업데이트 할 예정이다.


✨ 기존 JDBC 사용 방식 복기

과거에 JDBC API를 직접 사용할 땐 다음과 같은 절차가 필요했다:

  • DriverManager.getConnection()을 통해 Connection 직접 획득
  • SQL 실행 시마다 try-catch로 예외 처리
  • Connection, PreparedStatement, ResultSet 자원 수동 반납 필요
  • DAO 클래스에서 반복적인 코드가 많아짐

이러한 방식은 반복적이며 리소스 관리가 불편하고 실수를 유발할 수 있다.


JdbcTemplate

JdbcTemplate를 사용하면 리소스 획득, 연결 관리, 예외 처리 등 과 같은 작업은 고려하지 않고 쿼리와 응답 처리만 고민할 수 있게 도와준다.

기본적으로 DataSource를 사용하여 연결을 사용한다. 이때 DataSource는 기존에 연결하던 방식인 DriverManager의 대체제이다. Connection을 통해서 연결을 가능하게 해주는 인터페이스이다.

Spring에서 기본으로 사용하는 구현체는 HiKariDataSource이다. 이 구현체는 성능이 가장 좋으며 , Connection pool을 제공한다. connection 비용은 비싸기에 미리 여러개를 빌려둔다음에 반납하는 구조이다.


✨JdbcTemplate 사용을 위한 환경 설정

Jdbc를 사용하기 위해서는 앞서 몇가지 설정을 해주어야한다.

implementation 'org.springframework.boot:spring-boot-starter-jdbc'

jdbc 의존성을 추가하게 된다면 사용가능하다.

의존성을 추가해주게 되며 자연스럽게 DataSource Bean객체와 JdbcTemplate Bean 객체를 Spring에서 등록해주게 된다.

따라서 필요시 컨트롤러 생성자에서 주입을 해주거나 필드에서 @AutoWired을 통해 주입을 해주면 사용할 수 있다.

나는 이번 공부에서 연결한 DB는 H2 인메모리 DB이다.

application.properties에 다음과 같은 속성을 적용해주어야 한다.

spring.h2.console.enabled=true 
// 브라우저에서 localhost:8080/h2-console을 사용하겠다는 의미
spring.datasource.url=jdbc:h2:mem:test
//spring의 datasource 에 대한 연결을 할 주소를 설정하는 과정

보이는 것 과 같이 앞서 설명한 DataSourceConnection을 사용하여 연결할 주소를 설정해주는 모습이다.


✨JdbcTemplate Method

JdbcTemplate을 활용하여 이제 DB에 접근해보자. 제공해주는 메서드를 활용한다면 기존에 사용하던 jdbc Api 보다 더 편리한 기능을 제공받을 수 있다.

📃Querying(SELCT)

query 와 관련된 메서드는 SELECT를 사용하기 위해 사용한다.

queryForObject()

반환 값이 오직 하나일 경우 - 단일 객체 조회

쿼리문을 실행 후 반환하는 값이 단일일 경우에 사용하는 메서드이다.

만약 해당 객체가 없다면 EmptyResultDataAccessException를 발생시킨다.

위의 사진은 라이브러리에 정의되어 있는queryForObject인데 반환값이 단일 객체를 반환하도록 되어있다.

queryForObject 활용(1)

int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);
  • 첫번째 인자로는 쿼리문을 넣어준다.
  • 두번째 인자로는 실행된 값의 데이터 타입을 붙혀준다.

queryForObject 활용(2)

int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject(
		"select count(*) from t_actor where first_name = ?", Integer.class, "Joe");
  • 세번째 인자로 쿼리문의 파라미터와 바인딩할 값을 넣어주면 해당 쿼리문에 바인딩되어 실행된다.
  • 직접 값을 세번째 인자 이후로 ,를 사용하며 차례로 넣어주어도 되고, Object 배열안에 넣어주어도 차례로 바인딩 된다.

queryForObject 활용(3)

Actor actor = jdbcTemplate.queryForObject(
		"select first_name, last_name from t_actor where id = ?",
		(resultSet, rowNum) -> {
			Actor newActor = new Actor();
			newActor.setFirstName(resultSet.getString("first_name"));
			newActor.setLastName(resultSet.getString("last_name"));
			return newActor;
		},
		1212L);
  • 첫번째 인자는 Sql문을 넣어준다.
  • 두번째 인자로는 RowMapper
  • 세번째 인자로는 바인딩 될 값을 넣어주면 된다.

select 결과로 first_name과 last_name의 값이 나오게 되는데 이를 RowMapper를 통해 Actor 객체로 만들어주는 과정이다.

해당 람다식 안에서 반환되는 newActor를 그대로 queryForObject 반환값으로 사용한다. 따라서 select 를 통해서 2가지 칼럼값이 나왔지만 단일 객체로 반환하였기에 사용이 가능하다.

RowMapper에 관해서는 아래에서 더 자세히 다뤄보겠다.

칼럼의 개수가 여러개인 경우

칼럼이 하나일 때는 Spring이 ResultSet에서 그 단일 값을 자동으로 꺼내주는 내부 로직이 있어서 RowMapper 없이 바로 queryForObject(..., String.class) 처리가 가능하다.

하지만 칼럼이 두 개 이상이면 어떤 필드에 어떤 값을 넣어야 할지 모르기 때문에 반드시 **개발자가 직접 매핑 방식(RowMapper)을 알려줘야 한다.

  • 칼럼이 1개인 경우
int rowCount = jdbcTemplate.queryForObject("select count(*) from customers", Integer.class);
  • 칼럼이 2개 이상인 경우
String lastName = jdbcTemplate.queryForObject("select last_name from customers where id = ?", String.class, id);

이 경우는 학습을 위해 억지로 시도해본 경우이지만, 실제로 이런 방식으로 활용할 일은 없을 것 같다.

public String getLastName(Long id) {  
    //TODO : 주어진 Id에 해당하는 customers의 lastName을 반환  
    String sql = "SELECT last_name,first_name from customers WHERE id = ?";  
    String lastName = jdbcTemplate.queryForObject(sql,(rs,rowNum)->{return rs.getString("last_name");}, id);  
    return lastName;  
}
  • RowMapper를 전달하여 조회 결과를 매핑할 수 있다.
  
Customer customer = jdbcTemplate.queryForObject( "select id, first_name, last_name from customers where id = ?", (resultSet, rowNum) -> { Customer customer = new Customer( resultSet.getLong("id"), resultSet.getString("first_name"), resultSet.getString("last_name") ); return customer; }, id);

query()

0개 이상의 값들이 조회를 통해 나올 것을 기대하고 사용하는 메서드이다.
따라서 기존 queryForObject(sql,Stirng.class) 와 같은 단일 값 처리에 관해서는 사용이 불가능하다.

위의 사진과 같이 List 의 반환값을 보여주고 있다. 해당 리스트 안에서 0개이상의 값들을 넣어 반환하도록 사용할 수 있다.

query() 활용(1)

List<Actor> actors = this.jdbcTemplate.query(
		"select first_name, last_name from t_actor",
		(resultSet, rowNum) -> {
			Actor actor = new Actor();
			actor.setFirstName(resultSet.getString("first_name"));
			actor.setLastName(resultSet.getString("last_name"));
			return actor;
		});```
* 첫번째 값으로 sql 문을 넣어준다.
* 두번째 값으로는 `RowMapper`를 넣어주어 받은 값을 활용할 수 있다.




**query() 활용(2)**
```java
private final RowMapper<Customer> rowMapper = (rs, rowNum) -> {  
    Customer customer = new Customer(rs.getLong("id"), rs.getString("first_name"), rs.getString("last_name"));  
    return customer;  
};  
  
public List<Customer> findCustomerByFirstName(String firstName) {  
    String sql = "select id, first_name, last_name from customers where first_name = ?";  
    //TODO : firstName을 기준으로 customer를 list형태로 반환  
    List<Customer> customers = jdbcTemplate.query(sql, rowMapper, firstName);  
    return customers;  
}
  • 3번째 매개변수에 쿼리문에 바인딩할 파라미터를 지정할 수 있다.
  • 또한 RowMapper를 따로 재정의하여 사용할 수 있다.

⭐️RowMapper

RowMapperResultSet 의 값에 있는 데이터의 매핑을 도와주는 인터페이스이다.

쿼리문이 실행된다면 반환값으로 ResultSet에 값을 넣어서 데이터를 받아오게 된다.
이때 해당 jdbcTemplateResultSet 값에서 결과를 한줄 씩 RowMapper에게 넘겨주게 된다.
RowMappermapRow()메서드를 사용하며 데이터를 매핑하여 원하는 값으로 가공하는 기능을 제공한다.

mapRow()는 인자를 두가지 받아 작동하는데, 데이터 값이 들어있는 resultSet 과 해당이 몇번째 행인지 rowNum 이라는 값을 받게 된다. rowNum을 활용해 현재 매핑하고 있는 데이터가 몇번째인지에 대한 정보를 가지고 특정 상황의 분기점에서 활용할 수 있다.

정리해서, 기존 jdbc에서 while(resultSet.next())을 하며 resultSet.getString()을 하던 과정의 부분을 담당하는 인터페이스라고 생각하면 된다.


🔄UPDATE(INSERT,DELETE,UPDATE)

쿼리문 중 insert,delete,update를 하는 과정에서 사용된다.

update()

jdbcTemplate.update()를 통해서 값을 넣거나 삭제,혹은 수정을 할 수 있다.
모두 반환 값으로 해당 쿼리문으로 값이 변경된 테이블 행의 개수가 반환된다.

update() 활용(1)

jdbcTemplate.update(
		"insert into t_actor (first_name, last_name) values (?, ?)",
		"Leonor", "Watling");
  • 쿼리문 안에 넣을 파라미터 값을 차례로 넣어주면 된다.
  • 이때 역시 Object 배열 안에 값을 넣어준다면 차례로 바인딩된다.

🔑KeyHolder

쿼리의 insert문을 실행하게 된다면 DB에서 생성하여 넣어주는 값인 primary key인 Id 값이 필요한 경우가 있다.

이때 활용가능한 것이 KeyHolder이다. KeyHolder는 인터페이스인데 이때 주로 사용하는 구현체가 GeneratedKeyHolder 이다.

KeyHolder keyHolder = new GeneratedKeyHolder();

사용가능한 메서드로써는 다음과 같다.

메서드 이름반환 타입설명
getKey()Number첫 번째 키의 값만 단독으로 반환
주로 단일 키 (예: id)만 있는 경우 사용
getKeyAs(Class<T>)T (제네릭)getKey()의 타입 안전 버전
예: getKeyAs(Long.class)
getKeys()Map<String, Object>첫 번째 row의 키-값 전체 map 반환
getKeyList()List<Map<String, Object>>자동 생성 키들의 모든 row를 리스트로 반환
보통은 1개 row지만 여러 row도 가능

▶KeyHolder 사용

이제 공부한 KeyHolder를 사용해보자면

update() 활용(2)

jdbcTemplate.update(connection->{  
    PreparedStatement preparedStatement = connection.prepareStatement(sql,new String[]{"id"});  
    preparedStatement.setString(1, customer.getFirstName());  
    preparedStatement.setString(2,customer.getLastName());  
  
    return preparedStatement;  
},keyHolder);
  • 첫번째 인자로는 PreparedStatementConstructer가 필요하다.
  • 두번째 인자로는 앞서 생성한 KeyHolder 객체를 넣어주면 된다.

PreparedStatementConstructer 의 구현체를 뜯어보면

PreparedStatementCreator preparedStatementCreator = new PreparedStatementCreator() {  
    @Override  
    public PreparedStatement createPreparedStatement(Connection con) throws SQLException {  
        return null;  
    }  
};

createPreparedStateMent() 메서드를 구현해야한다.

이제 이 구현을 람다식을 통해서 update문 안에서 바로 정의하고 사용해보자.
바로 위에 있는 코드 update() 활용(2) 의 코드에서

connection->{  
    PreparedStatement preparedStatement = connection.prepareStatement(sql,new String[]{"id"});  
    preparedStatement.setString(1, customer.getFirstName());  
    preparedStatement.setString(2,customer.getLastName());  
  
    return preparedStatement;  
}

PreparedStatement를 connection을 통하여 생성하고 이때 값을 직접 주입해주게 된다.
prepareStatement() 메서드 안에는 2가지 인자가 필요한데

  • 첫번째로는 sql구문을 넣어준다.
  • 두번째로는 autoGenerated 가 되는 키값의 이름을 String 배열 안에 넣어주면 된다. autogenerated되는 값들 중에서 해당 이름을 가진 key에 대한 value값을 가져온다고 생각하면 된다. 혹은 key의 Index 번호값을 넣어줘도 가능하다.

prepareStatement()안에 인자를 넣어주고 반환하게 된다면,
jdbcTemplate.update()가 DB가 다시 반납해준 값을 keyHolder에 넣어주게 된다.

현재는 id 값 하나를 넣어주라고 부탁하였기 때문에 keyHolder에는 값 하나만 존재할 것이다.
따라서 keyHolder.getKey().longValue() 를 한다면 방금 가져온 자동생성된 값을 가져올 수 있다.

만약 prepareStatement(sql,new String[]{"id","id2"}) 와 같이 2개의 key값에 대한 요구를 한다면 keyHolder에 2개의 값이 저장되어 들어올 것이다. 이때 입력한 "id","id2"는 db에서 생성해주는 auotoGenerated 값이어야한다. 만약 값이 db에서 생성해주는 값이 아니라면 Null 이 들어있던가 특정 DB에서 예외가 터지게 된다.

다시 돌아와 만약 값 2개가 keyHolder에 잘 들어와 있다면

Map<String, Object> keys = keyHolder.getKeys();  // 내부적으로는 첫 번째 row의 keyMap

Long id = ((Number) keys.get("id")).longValue();
Long id2 = ((Number) keys.get("id2")).longValue();

을 통해서 들어있는 값을 가져올 수 있다. 값이 하나라면 이 부분들이 생략되어

keyHolder.getKey().longValue()

처럼 사용해주면 된다.

batchUpdate()

여러 값에 따른 여러 쿼리를 저장해두었다 한번에 모두 실행시키고 싶다면 사용한다.

batchUpdate 활용(1)

List<Object[]> splitUpNames = Arrays.asList("John Woo", "Jeff Dean", "Josh Bloch", "Josh Long")  
    .stream()  
    .map(name -> name.split(" "))  
    .collect(Collectors.toList());  
  
jdbcTemplate.batchUpdate("INSERT INTO customers(first_name, last_name) VALUES (?,?)", splitUpNames);
  • 첫번째 인자로는 쿼리문이 들어간다.
  • 두번째 인자로는 List<Object[]> 값이 들어가 List를 순회하며 리스트 안에 있는 각 배열의 값을 차례로 쿼리 문에 바인딩하여 실행한다.

☈EXECUTE(CREATE,DROP)

주로 단순한 CREATE,DROP을 sql문을 실행하기 위해 사용한다.
ConnectionCallback, PreparedStatementCallback 등을 직접 구현할 수 있다고 한다.
트랜잭션 경계 내에서 연결을 얻거나 세부 컨트롤 가능하다고 하는데 이는 추후에 공부하며 추가하겠다.

execute()

jdbcTemplate.execute("DROP TABLE customers IF EXISTS");
  • 첫번째 인자로 sql문을 넣어준다.

🧹정리

  • JdbcTemplate은 JDBC 반복 작업을 줄여주는 스프링의 헬퍼 클래스
  • queryForObject()는 단일 row 반환, query()는 리스트 반환
  • RowMapper는 DB 결과(ResultSet)를 Java 객체로 매핑해주는 전략
  • update()는 INSERT/UPDATE/DELETE 시 사용
  • KeyHolder를 사용하면 자동 생성된 키(id 등)를 받을 수 있음

참고 자료

profile
하루하루는 성실하게 인생 전체는 되는대로

6개의 댓글

comment-user-thumbnail
2025년 4월 21일

오 진짜 좋은데요???

1개의 답글
comment-user-thumbnail
2025년 4월 23일

공부하던 내용인데 잘 보고 갑니다😉👍

1개의 답글
comment-user-thumbnail
2025년 4월 24일

단일 레코드 조회 시(queryForObject 메서드) 결과가 없으면 어떻게 되나요?
(where절 만족하는 행이 없다거나, 테이블에 데이터가 아예 없다거나)
null을 반환하나요? 아니면 예외가 발생하나요?

1개의 답글