Spring

JdbcTemplate 사용 방법에 대해서

Tommy__Kim 2023. 7. 1. 18:58

JdbcTemplate 소개

직접 Jdbc을 사용하려는 경우 데이터 베이스와의 커넥션 연결 및 에러 났을 때에는 어떻게 할 것인지 등등...
개발자가 고려해야 하는 상황들이 많아지며, 추가적으로 비즈니스 로직 외적으로 구현해야 하는 코드의 양이 늘어나는 문제점이 존재합니다.
JdbcTemplate을 사용하면 비즈니스 로직 외적인 부분을 관리해 줍니다.
직접 SQL구문을 작성하는 경우 JdbcTemplate은 좋은 선택지가 될 수 있습니다.

JdbcTemplate의 장점 및 단점

장점

  1. 설정의 편리함
    JdbcTemplate은 spring-jdbc라이브러리에 포함되어 있습니다.
    스프링으로 JDBC를 사용하는 경우 기본으로 사용되는 라이브러리이며, 별도의 복잡한 설정 없이 사용할 수 있습니다.
  2. 반복 문제 해결
    JdbcTemplate은 템플릿 콜백 패턴을 사용해 대부분의 반복작업들을 처리해 줍니다.
    반복 작업에는 다음과 같은 내용들이 포함됩니다.
  • Connection 획득
  • statement를 준비하고 실행
  • 커넥션, statement, resultSet 종료
  • 커넥션 동기화
  • 예외 발생 시 예외 변환 실행

단점

  1. 동적 SQL을 해결하기 어렵다.
    개발자가 직접 동적 SQL을 구현하기 위해서는 고려해야 하는 상황들이 많아집니다.
    또한 String 기반으로 SQL을 작성하기 때문에 개발자의 실수가 잦을 수 있다는 단점이 존재합니다.

JdbcTemplate 쿼리 구현

JdbcTemplate 준비사항

jdbcTemplate을 사용하는 Repository에서 JdbcTemplate을 인스턴스 변수로 선언해 준 다음 DataSource를 사용해 JdbcTemplate을 생성해 주면 됩니다.

public class JdbcTemplateCustomerRepository implements CustomerRepository{

    private final JdbcTemplate jdbcTemplate;

    public JdbcTemplateCustomerRepository(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }
    // 데이터 저장 로직 ... 
}

데이터 저장 (save())

@Override
public Customer save(Customer customer) {
    String sql = "insert into customer(customer_name, age, phone_number) values (?, ?, ?)";
    GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
    jdbcTemplate.update(connection -> {
        PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"});
        ps.setString(1, customer.getCustomerName());        
        ps.setInt(2, customer.getAge());                    
        ps.setString(3, customer.getPhoneNumber());            
        return ps;
    }, keyHolder);
    long key = keyHolder.getKey().longValue();        
    customer.setId(key);                            
    return customer;
    }

데이터를 저장할 때에는 jdbctemplate.update()을 사용하면 됩니다.

{1}

GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();

해당 예제에서는 데이터베이스에 저장 시 customer의 Id값을 PK로 설정을 했으며, 데이터베이스가 Id를 생성할 수 있게끔 구현하였습니다.
database에 값을 저장 한 뒤에 id값을 가져올 수 있기에 keyHolder를 선언해 줍니다.

{2}

PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"});

connection.prepareStatement에 실행할 sql을 넣어주고, query가 실행된 후 데이터베이스에서 생성된 id값을 조회하기 위해 new String []{"id"}를 넣어줍니다.

{3}

ps.setString(1, customer.getCustomerName());        
ps.setInt(2, customer.getAge());                    
ps.setString(3, customer.getPhoneNumber());

sql에 ? 에 들어갈 값들을 설정해 줍니다.
변수들이 순서대로 들어가기 때문에 순서를 지켜서 값을 넣어주어야 합니다.

{4}

long key = keyHolder.getKey().longValue();        
customer.setId(key);                            

id값을 받아 customer의 id를 설정해 줍니다.

데이터 수정 (update())

@Override
public void update(Long customerId, CustomerUpdateDto updateParam) {
    String sql = "update customer set customer_name = ?, age = ?, phone_number = ? where id = ?";
    jdbcTemplate.update(sql,
      updateParam.getCustomerName(),
      updateParam.getAge(),
      updateParam.getPhoneNumber(),
      customerId);
}

데이터 베이스에 있는 값들을 수정할 때에도 마찬가지로 jdbcTemplate.update()를 사용해 주면 됩니다.
이때 실행할 sql을 파라미터로 넣어준 다음, sql에 ? 에 해당하는 부분들을 차례로 넣어주면 됩니다.

데이터 단건 조회 (findById())

@Override
  public Optional<Customer> findById(Long customerId) {
    String sql = "select * from customer where id = ?";
    try {
      Customer customer = jdbcTemplate.queryForObject(sql, customerRowMapper(), customerId);
      return Optional.of(customer);
    } catch (EmptyResultDataAccessException e) {
      return Optional.empty();
  }
}

--- 

private RowMapper<Customer> customerRowMapper() {
    return (((rs, rowNum) -> {
        Customer customer = new Customer();
        customer.setId(rs.getLong("id"));
        customer.setCustomerName(rs.getString("customer_name"));
        customer.setAge(rs.getInt("age"));
        customer.setPhoneNumber(rs.getString("phone_number"));
        return customer;
    } ));
}

결과를 하나만 가져오는 경우 jdbcTemplate.queryForObject를 사용하면 됩니다.

queryForObject에서 예외 발생 상황

queryForObject의 경우 두 가지 상황에 대해 예외가 발생합니다.

  • EmptyResultDataAccessException : 결과가 없는 경우
  • IncorrectResultSizeDataAccessException : 결과가 두 개 이상인 경우

RowMapper

@Override
@Nullable
public <T> T queryForObject(String sql, RowMapper<T> rowMapper, @Nullable Object... args) throws             DataAccessException {
    List<T> results = query(sql, args, new RowMapperResultSetExtractor<>(rowMapper, 1));
    return DataAccessUtils.nullableSingleResult(results);
}

queryForObject는 Primitive 타입이 아닌 결과를 조회할 때, 반환된 결과를 객체로 만들어주는 RowMapper가 필요합니다.
사용자 비즈니스에 맞춰 적절한 객체 타입을 반환할 수 있도록 구현을 해주면 됩니다.
customerRowMapper()에서 rs는 resultSet(query를 통해 조회해 온 결과)를 의미하며, rowNum은 조회된 결과 중 몇 번째 row인지 나타내줍니다.

findAll()

@Override
public List<Customer> findAll() {
  String sql = "select * from customer";
  return jdbcTemplate.query(sql, customerRowMapper());
}

여러 가지 데이터를 조회하는 경우 jdbcTemplate.query()를 사용합니다.

JdbcTemplate 검증

@Transactional
@SpringBootTest
class JdbcTemplateCustomerRepositoryTest {

    @Autowired
    CustomerRepository customerRepository;

    @Test
    void save() {
        // given
        Customer customer = new Customer("Tommy", 20, "010-1234-5678");

        // when
        Customer savedCustomer = customerRepository.save(customer);

        // then
        Customer findCustomer = customerRepository.findById(savedCustomer.getId()).get();
        Assertions.assertThat(findCustomer).isEqualTo(savedCustomer);
    }

    @Test
    void updateCustomer() {
        // given
        Customer customer = new Customer("Tommy", 20, "010-1234-5678");
        Customer savedCustomer = customerRepository.save(customer);
        Long customerId = savedCustomer.getId();

        // when
        CustomerUpdateDto updateParam = new CustomerUpdateDto("new Tommy", 30, "010-9876-5432");
        customerRepository.update(customerId, updateParam);

        // then
        Customer findCustomer = customerRepository.findById(customerId).get();
        assertThat(findCustomer.getCustomerName()).isEqualTo("new Tommy");
        assertThat(findCustomer.getAge()).isEqualTo(30);
        assertThat(findCustomer.getPhoneNumber()).isEqualTo("010-9876-5432");
    }

    @Test
    void findAll() {

        // given
        Customer customer1 = new Customer("Tommy", 20, "010-1234-5678");
        Customer customer2 = new Customer("Tommy1", 21, "010-1234-5678");
        Customer customer3 = new Customer("Tommy2", 22, "010-1234-5678");

        // when
        customerRepository.save(customer1);
        customerRepository.save(customer2);
        customerRepository.save(customer3);

        // then
        assertThat(customerRepository.findAll().size()).isEqualTo(3);
    }
}

개발한 기능에 대해 저장, 업데이트, 전체 조회에 대해서 테스트를 해본 결과 모두 정상적으로 동작함을 확인했습니다!

다음번에는 JdbcTemplate을 사용하는 경우의 위험성과, 이를 보완할 수 있는 NamedParameterJdbcTemplate에 대해 알아보도록 하겠습니다.


해당 예제에 대한 코드는 다음 링크에서 확인하실 수 있습니다.

예제코드 바로가기