JdbcTemplate

초코칩·2024년 4월 22일
0

spring

목록 보기
8/10
post-thumbnail

JdbcTemplate이란?

JdbcTemplate은 스프링 프레임워크에서 제공하는 JDBC(Java Database Connectivity) 작업을 보다 쉽게 처리할 수 있도록 도와주는 클래스다. JDBC는 자바 언어를 통해 데이터베이스에 접근하고 SQL 쿼리를 실행하는 데 사용되는 표준 API다. 그러나 JDBC를 직접 사용할 경우에는 반복적인 작업과 예외 처리 등으로 인해 코드가 복잡해질 수 있다.

JdbcTemplate 적용 전

아래 코드는 순수 JDBC를 이용한 코드다.

@Override
public void save(final long roomId, final long pieceId, final Square square) {
	final String query = "INSERT INTO board (room_id, square, piece_id) VALUES (?, ?, ?)";
	final Connection connection = getConnection();
	try (final PreparedStatement preparedStatement = connection.prepareStatement(query,
                Statement.RETURN_GENERATED_KEYS)) {
		preparedStatement.setLong(1, roomId);
		preparedStatement.setString(2, square.getName());
		preparedStatement.setLong(3, pieceId);
		preparedStatement.executeUpdate();
	} catch (SQLException exception) {
		throw new RuntimeException(exception);
	} finally {
		closeConnection(connection);
	}
}

코드에서 알 수 있듯이 많은 양의 코드를 작성해야 한다. 뿐만 아니라 매 쿼리마다 Connection을 불러오고 SQL의 파라미터를 매핑해야 하는 수고로움이 존재한다.

JdbcTemplate 적용 후

JdbcTemplate은 이러한 문제를 해결하기 위해 JDBC 작업을 단순화하고, 반복적인 코드를 줄여주는 역할을 한다.

@Override
public void save(final long roomId, final long pieceId, final Square square) {
    final String query = "INSERT INTO board (room_id, square, piece_id) VALUES (?, ?, ?)";

    jdbcTemplate.update(query, roomId, square.getName(), pieceId);
}

주요 기능

주요 기능은 다음과 같다.

  1. SQL 쿼리 실행: JDBC를 사용하여 데이터베이스에 SQL 쿼리를 실행할 수 있다. JdbcTemplate을 사용하면 SQL 쿼리를 실행하고 결과를 처리하는 과정이 간결해진다.

  2. 파라미터 바인딩: SQL 쿼리에 파라미터를 전달하여 동적으로 쿼리를 생성할 수 있다. JdbcTemplate은 파라미터 바인딩을 지원하여 보안과 코드의 유연성을 향상시킨다.

  3. 예외 처리: JDBC 작업 중 발생할 수 있는 예외를 처리하기 위한 메커니즘을 제공한다. JdbcTemplate은 JDBC 예외를 스프링의 DataAccessException으로 변환하여 편리한 예외 처리를 제공한다.

  4. 트랜잭션 관리: 스프링의 트랜잭션 매니저와 함께 사용하여 데이터베이스 트랜잭션을 관리할 수 있다. JdbcTemplate은 트랜잭션 경계를 설정하고 롤백 및 커밋을 처리하는 데 도움을 준다.

JdbcTemplate을 사용하면 JDBC 작업을 보다 간편하게 처리할 수 있으며, 스프링의 핵심 기능과 통합하여 개발 생산성을 향상시킬 수 있다.

JdbcTemplate 사용하기

JdbcTemplate 클래스를 활용한 JDBC 프로그래밍의 방법에 대해 알아보자.

JdbcTemplate 초기화

DAO 클래스에서 JdbcTemplate을 초기화하는지 알아보자. 대부분 애플리케이션에서는 스프링이 DAO 클래스에 Bean에 주입해야 하는 DataSource 객체를 생성자에 넘긴 뒤 JdbcTemplate 인스턴스를 만든다.

public class JdbcTemplate {
	  public JdbcTemplate(DataSource dataSource) {
        this.setDataSource(dataSource);
        this.afterPropertiesSet();
    }

    public JdbcTemplate(DataSource dataSource, boolean lazyInit) {
        this.setDataSource(dataSource);
        this.setLazyInit(lazyInit);
        this.afterPropertiesSet();
    }
}

스프링이 DataSource를 주입하면 JdbcTemplate도 초기화돼 사용할 수 있다.

단일값 조회 - queryForObject

JdbcTemplate의 queryForObject 메서드를 이용하여 단일 객체를 조회할 수 있다.

public ReservationTime findById(long timeId) {
	String sql = "SELECT id, start_at FROM reservation_time WHERE id = ?";
	return jdbcTemplate.queryForObject(sql, ReservationTime.class, timeId);
}

첫 번째 매개변수는 쿼리문이며, 두 번째 매개변수는 조회 결과를 매핑할 클래스 타입, 세 번째 매개변수를 이용하여 쿼리문에 바인딩할 파라미터를 전달할 수 있다.

KeyHolder

KeyHolder는 JDBC 작업을 수행할 때 자동으로 생성되는 DB의 키(Auto-Increment)를 보관하는 인터페이스다. 주로 INSERT 문을 실행한 후에 자동으로 생성된 키 값을 가져오는 데 사용된다.

KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> {
    PreparedStatement ps = connection.prepareStatement(
            "insert into customers (first_name, last_name) values (?, ?)", 
            new String[]{"id"});
    ps.setString(1, customer.getFirstName());
    ps.setString(2, customer.getLastName());
    return ps;
}, keyHolder);

Long id = keyHolder.getKey().longValue();

GeneratedKeyHolder 클래스는 내부적으로 ArrayList로 구현되어 있고, ArrayList는 Thread-Safe하지 않기 때문에 주의해야 한다.

public class GeneratedKeyHolder implements KeyHolder {
    private final List<Map<String, Object>> keyList;
    
    public GeneratedKeyHolder() {
        this.keyList = new ArrayList(1);
    }
    
    //...
}

SimpleJdbcInsert

하지만 여전히 위 KeyHolder를 이용한 방법은 가독서이 많이 떨어진다. 내부에 try-catch가 들어가게 된다면 가독성이 더 안좋아진다.

JdbcTemplate은 insert를 위해 SimpleJdbcInsert를 제공한다. 사용하기 위해서는 DataSource를 주입해야 한다.

@Repository
public class ReservationDao implements ReservationRepository {
    private final JdbcTemplate jdbcTemplate;
    private final SimpleJdbcInsert simpleJdbcInsert;
    private final KeyHolder keyHolder = new GeneratedKeyHolder();

    public ReservationDao(JdbcTemplate jdbcTemplate, DataSource dataSource) {
        this.jdbcTemplate = jdbcTemplate;
        this.simpleJdbcInsert = new SimpleJdbcInsert(dataSource)
                .withTableName("reservation")
                .usingGeneratedKeyColumns("id");
    }
}

위 KeyHolder의 예시 코드를 다음과 같이 변경할 수 있다.

    @Override
    public Reservation save(final Reservation reservation) {
        Map<String, Object> params = new HashMap<>();
        params.put("name", reservation.getName());
        params.put("date", reservation.getDate());
        params.put("time_id", reservation.getTime().getId());

        long id = simpleJdbcInsert.executeAndReturnKey(params).longValue();
        return new Reservation(
                id,
                reservation.getName(),
                reservation.getDate(),
                reservation.getTime()
        );
    }

코드가 획기적으로 감소하고, 가독성도 좋아진 것을 느낄 수 있다. 다만, 저장해야할 칼럼이 많아지면 직접 Map으로 put()하는 방식은 비효율적일 수 있다. 이를 위해 SqlParameterSource를 제공한다.

MapSqlParameterSource

SqlParameterSource의 구현체 중 하나인 MapSqlParameterSource를 사용하면 Map parameter 생성 시, 메서드 체이닝으로 가독성 있게 생성할 수 있다.

    @Override
    public Reservation save(final Reservation reservation) {
        SqlParameterSource params = new MapSqlParameterSource()
                .addValue("name", reservation.getName())
                .addValue("date", reservation.getDate())
                .addValue("time_id", reservation.getTime().getId());
        
        long id = simpleJdbcInsert.executeAndReturnKey(params).longValue();
        return new Reservation(
                id,
                reservation.getName(),
                reservation.getDate(),
                reservation.getTime()
        );
    }

BeanPropertySqlParameterSource

SqlParameterSource의 구현체 중 하나인 BeanPropertySqlParameterSource를 사용할 수 있다. BeanPropertySqlParameterSource는 이전 방식인 MapSqlParameterSource보다 더 간단하게 사용할 수 있지만, Bean으로 등록 가능한 객체에만 적용할 수 있다.

Bean으로 등록 가능하기 위해서는 (getter 메서드) + (생성자 또는 setter 메서드)가 필요하다.

    @Override
    public Reservation save(final Reservation reservation) {
        SqlParameterSource params = new BeanPropertySqlParameterSource(reservation);
        long id = simpleJdbcInsert.executeAndReturnKey(params).longValue();
        return new Reservation(
                id,
                reservation.getName(),
                reservation.getDate(),
                reservation.getTime()
        );
    }

RowMapper<T>

일반적으로 DB에서 조회를 할 때는 값 하나만 조회하기보다는 한 row나 여러 row를 질의한 후 각 row를 도메인 객체나 엔티티로 변환해 사용한다. 스프링의 RowMapper<T>를 사용하면 JDBC ResultSet을 간한 POJO 객체로 매핑할 수 있다.

List<Customer> customers = jdbcTemplate.query(
        "select id, first_name, last_name from customers",
        (resultSet, rowNum) -> {
            Customer customer = new Customer(
                    resultSet.getLong("id"),
                    resultSet.getString("first_name"),
                    resultSet.getString("last_name")
            );
            return customer;
        });

queryForObject와 마찬가지로 세 번째 매개변수를 이용하여 쿼리문에 바인딩할 파라미터를 전달할 수 있다.

RowMapper의 경우 별도로 선언하여 사용할 수 있다.

private final RowMapper<Customer> rowMapper = (resultSet, rowNum) -> {
    Customer customer = new Customer(
            resultSet.getLong("id"),
            resultSet.getString("first_name"),
            resultSet.getString("last_name"));
    return customer;
};


public List<Customer> findFirstName() {
	List<Customer> customers = jdbcTemplate.query(
    	"select id, first_name, last_name from customers where first_name = ?", 
    	rowMapper, firstName
    );
}

별도로 RowMapper로 사용할 경우, 중복된 코드를 줄일 수 있다.

update 메서드

JdbcTemplate의 update 메서드를 이용하여 INSERT, UPDATE, DELETE 쿼리를 실행할 수 있다.

아래는 update 메서드를 이용해 DELETE 쿼리를 실행하는 함수이다.

public boolean deleteById(long timeId) {
	String sql = "DELETE FROM reservation_time WHERE id = ?";
	int deleteId = jdbcTemplate.update(sql, timeId);
	return deleteId != 0;
}

BatchSqlUpdate

JDBC 드라이버는 동일한 여러 호출을 일괄 처리할 경우 성능이 향상된다. Update를 일괄 처리로 그룹화하기 batch를 활용할 수 있다.

Batch는 jdbcTemplate에서 batchUpdate()를 활용하여 구현할 수 있다.

public int[] batchUpdate(final List<ReservationTime> times) {
	return this.jdbcTemplate.batchUpdate(
		"update reservation_time set start_at = ? where id = ?",
			new BatchPreparedStatementSetter() {
				public void setValues(PreparedStatement ps, int i) throws SQLException {
					ReservationTime time = times.get(i);
					ps.setTime(1, Time.valueOf(time.getStartAt()));
					ps.setLong(2, time.getId());
				}

				public int getBatchSize() {
					return times.size();
				}
		}
	);
}

Ref

profile
초코칩처럼 달콤한 코드를 짜자

0개의 댓글