데이터 접근 기술 - 스프링 JdbcTemplate

2025. 10. 18. 02:35·Spring

JdbcTemplate 적용1 - 기본

JdbcTemplate을 사용해서 메모리에 저장하던 데이터를 DB에 저장해보자

ItemRepository 인터페이스를 기반으로 jdbcTemplate을 사용하는 새로운 구현체를 개발해보자

@Slf4j
public class JdbcTemplateItemRepositoryV1 implements ItemRepository {

    private final JdbcTemplate template;

    public JdbcTemplateItemRepositoryV1(DataSource dataSource) {
        this.template = new JdbcTemplate(dataSource);
    }

    @Override
    public Item save(Item item) {
        String sql = "insert into item(item_name, price, quantity) values (?,?,?)";
        KeyHolder keyHolder = new GeneratedKeyHolder();
        template.update(connection -> {
            // 자동 증가 키
            PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"});
            ps.setString(1, item.getItemName());
            ps.setInt(2, item.getPrice());
            ps.setInt(3, item.getQuantity());
            return ps;
        }, keyHolder);

        long key = keyHolder.getKey().longValue();
        item.setId(key);
        return item;
    }

    @Override
    public void update(Long itemId, ItemUpdateDto updateParam) {
        String sql = "update item set item_name=?, price=?, quantity=? where id=?";
        template.update(sql,
                updateParam.getItemName(),
                updateParam.getPrice(),
                updateParam.getQuantity(),
                itemId);
    }

    @Override
    public Optional<Item> findById(Long id) {
        String sql = "select id, item_name, price, quantity from item here id = ?";
        try {
            Item item = template.queryForObject(sql, itemRowMapper(), id);
            return Optional.of(item);
        } catch (EmptyResultDataAccessException e) {
            return Optional.empty();
        }
    }

    @Override
    public List<Item> findAll(ItemSearchCond cond) {
        String itemName = cond.getItemName();
        Integer maxPrice = cond.getMaxPrice();

        String sql = "select id, item_name, price, quantity from item";
        // 동적 쿼리
        if (StringUtils.hasText(itemName) || maxPrice != null) {
            sql += " where";
        }
        boolean andFlag = false;
        List<Object> param = new ArrayList<>();
        if (StringUtils.hasText(itemName)) {
            sql += " item_name like concat('%',?,'%')";
            param.add(itemName);
            andFlag = true;
        }
        if (maxPrice != null) {
            if (andFlag) {
                sql += " and";
            }
            sql += " price <= ?";
            param.add(maxPrice);
        }
        log.info("sql={}", sql);
        return template.query(sql, itemRowMapper(), param.toArray());
    }

    private RowMapper<Item> itemRowMapper() {
        return ((rs, rowNum) -> {
            Item item = new Item();
            item.setId(rs.getLong("id"));
            item.setItemName(rs.getString("item_name"));
            item.setPrice(rs.getInt("price"));
            item.setQuantity(rs.getInt("quantity"));
            return item;
        } );
    }
}

 

- JdbcTemplateItemRepositoryV1은 ItemRepository를 구현했다.

- this.template = new JdbcTemplate(dataSource) 

 JdbcTemplate은 dataSource가 필요하다.

생성자를 보면 dataSource를 의존관계 주입 받고 생성자 내부에서 JdbcTemplate을 생성한다. 스프링에서 관례상 이 방법을 주로 사용한다.

 

save()

- template.update() 데이터를 변경할 땐 update()를 사용하면 된다. (INSERT, UPDATE, DELETE SQL에 사용)

- PK 생성에 identity(auto increment)방식을 사용하기 때문에, PK인 ID값을 개발자가 직접 지정하는 것이 아니라 비워두고 저장해야 한다. 그러면 DB가 PK인 ID를 대신 생성해준다.

- 문제는 DB가 대신 생성해주는 PK값은 DB가 생성하기 때문에, INSERT가 완료되어야 생성된 PK ID값을 확인할 수 있다.

- keyHolder와 connection.prepareStatement(sql, new String[] ...)을 사용해서 id를 지정해주면 INSERT 쿼리 실행 이후에 DB에서 생성된 ID값을 조회할 수 있다.

- 뒤에 SimpleJdbcInsert라는 기능을 사용하면 편하게 사용할 수 있다. 

 

*이해 안가서 검색한 내용 (template.update())

- 내부에서 connection으로 PreparedStatement 생성

- template.update() 메서드 내부에 ps.executeUpdate(); 실행 시점에 PK 생성됨

- update() 내부에 RowMapperResultSetExtractor가 있는데, 이게 ResultSet을 List<Map<String,Object>>로 변환

- 변환한 리스트를 generatedKeys, 즉 내가 넘긴 keyHolder의 내부 리스트에 addAll로 그대로 집어 넣게 됨

- 아래쪽 로직에서 keyHolder에 있는 key를 빼와서 item.setId로 key를 세팅하게 되는 코드임

 

update()

- template.update() : 변경할때도 마찬가지로 update() 사용

- ?에 바인딩할 파라미터를 순서대로 전달하면 된다.

 

findById()
데이터 단건 조회

- template.queryForObject() : 결과 로우가 하나일 때 사용, RowMapper는 ResultSet을 객체로 변환

결과가 없다면 EmptyResultDataAccessException 예외 발생, 둘 이상이면 IncorrectResultSizeDataAccessException 발생

- ItemRepository.findByID() 인터페이스는 결과가 없을 때 Optional을 반환 해야한다. 결과가 없다면 예외를 잡아서 Optional.empty 반환하면 된다.

 

findAll()

데이터를 리스트로 조회, 검색 조건 사용

- template.query() : 결과가 하나 이상일 때 사용 

- 결과가 없으면 빈 컬렉션 반환

- 동적 쿼리 부분은 일단 패스

 

JdbcTemplate 적용2 - 동적 쿼리 문제

findAll()에서 사용자가 검색하는 값에 따라서 실행하는 SQL이 동적으로 달라져야 한다.

// 검색 조건 없음
select id, item_name, price, quantity from item

// 상품명(itemName)으로 검색
select id, item_name, price, quantity from item
where item_name like concat('%',?,'%')

// 최대 가격(maxPrice)으로 검색
select id, item_name, price, quantity from item
where price <= ?

// 상품명(itemName), 최대 가격(maxPrice) 둘 다 검색
select id, item_name, price, quantity from item
where item_name like concat('%',?,'%')
and price <= ?

 

결과적으로 4가지 상황에 따른 SQL을 동적으로 생성해야 되는데, 이게 정말 어렵다.

MyBatis를 사용할 때 큰 장점은 동적 쿼리를 쉽게 작성할 수 있다.

 

JdbcTemplate 적용3 - 구성과 실행

package hello.itemservice.config;

import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.jdbctemplate.JdbcTemplateItemRepositoryV1;
import hello.itemservice.service.ItemService;
import hello.itemservice.service.ItemServiceV1;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
@RequiredArgsConstructor
public class JdbcTemplateV1Config {

    private final DataSource dataSource;

    @Bean
    public ItemService itemService() {
        return new ItemServiceV1(itemRepository());
    }

    @Bean
    public ItemRepository itemRepository() {
        return new JdbcTemplateItemRepositoryV1(dataSource);
    }
}

 

ItemRepository 구현체로 JdbcTemplateItemRepositoryV1이 사용되도록 했따. 이제 메모리 저장소가 아니라 DB에 연결하는 JdbcTemplate이 된다.

 

참고로 쿼리 로그를 확인하려면 로깅레벨을 debug로 설정할것

 

JdbcTemplate - 이름 지정 파라미터 1

JdbcTemplate을 기본으로 사용하면 파라미터를 순서대로 바인딩 한다. (? 순서를 보자)

순서만 잘 지키면 상관없다. 그러나 변경 시점에 문제가 발생할 수 있다.

누군가 SQL 코드 순서를 변경했다고 가정했을 때, 맞지 않는 값이 바인딩 될 수 있다.

DB에 데이터가 잘 못 들어가는 버그가 생기면 어마어마한 리소스가 발생한다.

 

이름 지정 바인딩

NamedParameterJdbcTemplate을 사용하면 이런 문제를 사전에 방지할 수 있다. 이름을 지정해서 파라미터를 바인딩 한다.

// NamedParameterJdbcTemplate
@Slf4j
public class JdbcTemplateItemRepositoryV2 implements ItemRepository {

//    private final JdbcTemplate template;
    private final NamedParameterJdbcTemplate template;
    public JdbcTemplateItemRepositoryV2(DataSource dataSource) {
        this.template = new NamedParameterJdbcTemplate(dataSource);
    }

    @Override
    public Item save(Item item) {
        String sql = "insert into item (item_name, price, quantity) " +
                "values (:itemName, :price, :quantity)";
        SqlParameterSource param = new BeanPropertySqlParameterSource(item);

        KeyHolder keyHolder = new GeneratedKeyHolder();
        template.update(sql, param, keyHolder);

        long key = keyHolder.getKey().longValue();
        item.setId(key);
        return item;
    }

    @Override
    public void update(Long itemId, ItemUpdateDto updateParam) {
        String sql = "update item " +
                "set item_name=:itemName, price=price, quantity=:quantity " +
                "where id=:id";

        SqlParameterSource param = new MapSqlParameterSource()
                .addValue("itemName", updateParam.getItemName())
                .addValue("price", updateParam.getPrice())
                .addValue("quantity", updateParam.getQuantity())
                .addValue("id", itemId);

        template.update(sql, param);
    }

    @Override
    public Optional<Item> findById(Long id) {
        String sql = "select id, item_name, price, quantity from item where id=:id";
        try {
            Map<String, Object> param = Map.of("id", id);
            Item item = template.queryForObject(sql, param, itemRowMapper());
            return Optional.of(item);
        } catch (EmptyResultDataAccessException e) {
            return Optional.empty();
        }
    }

    @Override
    public List<Item> findAll(ItemSearchCond cond) {
        String itemName = cond.getItemName();
        Integer maxPrice = cond.getMaxPrice();

        SqlParameterSource param = new BeanPropertySqlParameterSource(cond);

        String sql = "select id, item_name, price, quantity from item";
        // 동적 쿼리
        if (StringUtils.hasText(itemName) || maxPrice != null) {
            sql += " where";
        }
        boolean andFlag = false;
        if (StringUtils.hasText(itemName)) {
            sql += " item_name like concat('%',:itemName,'%')";
            andFlag = true;
        }
        if (maxPrice != null) {
            if (andFlag) {
                sql += " and";
            }
            sql += " price <= :maxPrice";
        }

        log.info("sql={}", sql);
        return template.query(sql, param, itemRowMapper());
    }

    private RowMapper<Item> itemRowMapper() {
        return BeanPropertyRowMapper.newInstance(Item.class);
    }
}

 

this.template = new NamedParameterJdbcTemaplte(dataSource)

- NamedParameterJdbcTemplate도 내부에 dataSource가 필요하다.

 

각 메서드를 보면 ? 대신에 :파라미터이름을 받는것을 확인할 수 있다.

추가로 데이터베이스가 생성해주는 키를 매우 쉽게 조회하는 기능도 제공한다.

 

JdbcTemplate - 이름 지정 파라미터 2

이름 지정 파라미터

파라미터를 전달하려면 Map처럼 key, value 데이터 구조를 만들어서 전달해야 한다.

여기서 key는 :파라미터이름, value는 그 값이다.

 

template.update(sql, param, keyHolder); < 만든 파라미터를 전달하는것을 확인할 수 있다.

 

이름 지정 바인딩에서 자주 사용하는 파라미터 종류는 크게 3가지가 있다.

- Map

- SqlParameterSource

(MapSqlParameterSource, BeanPropertySqlParameterSource)

 

1. Map

단순히 Map을 사용한다.

Map<String, Object> param = Map.of("id", id);
Item item = template.queryForObject(sql, param, itemRowMapper());

 

2. MapSqlParameterSource - update() 에서 확인

Map과 유사한데, SQL 타입을 지정할 수 있는 등 SQL에 좀 더 특화된 기능 제공

메서드 체인을 통해 편리한 사용법도 제공됨

 

3. BeanPropertySqlParameterSource - save(), findAll() 에서 확인

자바빈 프로퍼티 규약을 통해서 자동으로 파라미터 객체를 생성한다.

예를 들어 getItemName(), getPrice() 가 있다면

key = itemName, value = 상품명 / key = price, value = 가격

 

많은것을 자동화해주기 때문에 가장 좋아보이지만, 항상 사용할 순 없다.

update()에서는 SQL에 :id 바인딩을 해야하는데, update()에서 사용하는 itemUpdateDto엔 itemId가 없다.

이런 경우엔 BeanPropertySqlParameterSource를 사용할 수 없고, 대신 MapSqlParameterSource를 사용한다.

 

BeanPropertyRowMapper

private RowMapper<Item> itemRowMapper() {
    return ((rs, rowNum) -> {
        Item item = new Item();
        item.setId(rs.getLong("id"));
        item.setItemName(rs.getString("item_name"));
        item.setPrice(rs.getInt("price"));
        item.setQuantity(rs.getInt("quantity"));
        return item;
    } );
}

// BeanPropertyRowMapper 사용
private RowMapper<Item> itemRowMapper() {
    return BeanPropertyRowMapper.newInstance(Item.class);
}

 

BeanPropertyRowMapper는 ResultSet의 결과를 받아서 자바빈 규약에 맞춰 데이터를 변환한다.

db 조회결과가 select id, price라고 한다면

Item item = new Item();
item.setId(rs.getLong("id"));
item.setPrice(rs.getInt("price"));

 

이런 코드를 작성해준다. 데이터베이스에서 조회한 결과 이름을 기반으로 setId(), setPrice()처럼 자바빈 프로퍼티 규약에 맞춘 메서드를 호출하는 것이다.

 

별칭

관례의 불일치

자바 객체는 카멜 표기법을 사용하고, 관계형 데이터베이스는 주로 스네이크 케이스 표기법을 사용한다.

이 부분을 관례로 많이 사용하다보니 beanPropertyRowMapper는 언더스코어 표기법을 자동으로 카멜로 변환해준다.

조회 SQL에서 별칭을 사용할 땐 (as)를 사용하는데, 컬럼 이름과 객체 이름이 완전히 다른 경우에 사용하면 된다.

(컬럼은 memberName인데 객체는 userName일 때)

 

JdbcTemplate - SimplejdbcInsert

INSERT SQL을 직접 작성하지 않아도 되도록 simpleJdbcInsert라는 기능을 제공한다.

 

public class JdbcTemplateItemRepositoryV3 implements ItemRepository {

    private final NamedParameterJdbcTemplate template;
    private final SimpleJdbcInsert jdbcInsert;

    public JdbcTemplateItemRepositoryV3(DataSource dataSource) {
        this.template = new NamedParameterJdbcTemplate(dataSource);
        this.jdbcInsert = new SimpleJdbcInsert(dataSource)
                .withTableName("item")
                .usingGeneratedKeyColumns("id");
//                .usingColumns("item_name", "price", "quantity"); 생략 가능
    }

    @Override
    public Item save(Item item) {
        BeanPropertySqlParameterSource param = new BeanPropertySqlParameterSource(item);
        Number key = jdbcInsert.executeAndReturnKey(param);
        item.setId(key.longValue());
        return item;
    }
}

 

withTableName : 데이터를 저장할 테이블 명 지정

usingGeneratedKeyColumns : key를 생성하는 PK 컬럼명 지정

usingColumns : insert sql에 사용할 컬럼 지정. 특정 값만 저장하고 싶을때 사용

 

생성 시점에 데이터베이스 테이블의 메타 데이터를 조회해서 어떤 컬럼이 있는지 확인할 수 있으므로 usingColumns를 생략할 수 있는것이다.

 

동작 구조

1. 테이블 이름 등록

2. 자동 증가 컬럼 지정

3. 파라미터 매핑 (BeanPropertySqlParameterSource로 전달된 item 객체 프로퍼티 기반으로)

- 내부적으로 MapSqlParameterSource 형태로 변환됨 ({itemName=AA, price=1000, quantity=3}

4. SQL 자동 생성

5. PreparedStatement 실행, keyHolder 채움

6. 반환

 

 

JdbcTemplate 핵심

  • 역할: JDBC의 반복작업(커넥션/PreparedStatement 생성·파라미터 바인딩·실행·리소스 정리·예외변환)을 대신 처리.
  • 필수 주입: DataSource → new JdbcTemplate(dataSource).

CRUD 패턴

  • INSERT: template.update(...) + KeyHolder로 DB가 생성한 PK까지 회수 가능.
  • UPDATE/DELETE: template.update(sql, args...).
  • SELECT 단건: template.queryForObject(sql, rowMapper, args...) (없으면 EmptyResultDataAccessException).
  • SELECT 목록: template.query(sql, rowMapper, args...) (없으면 빈 리스트).

RowMapper

  • ResultSet → 도메인 변환기.
  • 수동 매핑: 람다로 new Item() 채우기.
  • 자동 매핑: BeanPropertyRowMapper.newInstance(Item.class)
  • (스네이크 → 카멜 자동 변환, 컬럼명이 다르면 SQL 별칭 AS 사용).

NamedParameterJdbcTemplate (이름 기반 바인딩)

  • 동기: ? 순서 실수 방지, 가독성↑.
  • 사용법: :itemName, :price 같은 이름 바인딩.
  • 파라미터 전달 3형식
    1. Map<String,Object> (가장 단순)
    2. MapSqlParameterSource (체이닝/SQL 타입 지정)
    3. BeanPropertySqlParameterSource (DTO/엔티티의 getter를 읽어 자동 매핑)

SimpleJdbcInsert (INSERT 자동 생성)

  • 장점: INSERT SQL 직접 안 써도 됨.
    • 원리: 테이블 메타데이터 읽어 컬럼 파악 → SQL 생성 → 실행 → KeyHolder 채워 반환.

 

* 보강내용 - 예외처리까지 자동화된다.

  • 스프링이 SQLException을 DataAccessException 계열로 일관 변환.
  • (DB 벤더별 에러코드도 SQLErrorCodeSQLExceptionTranslator로 추상화)

'Spring' 카테고리의 다른 글

데이터 접근 기술 - MyBatis  (0) 2025.10.21
데이터 접근 기술 - 테스트  (0) 2025.10.20
스프링과 문제 해결 - 예외처리, 반복  (0) 2025.10.17
자바 예외 이해  (0) 2025.10.16
트랜잭션 2  (0) 2025.10.13
'Spring' 카테고리의 다른 글
  • 데이터 접근 기술 - MyBatis
  • 데이터 접근 기술 - 테스트
  • 스프링과 문제 해결 - 예외처리, 반복
  • 자바 예외 이해
공부처음하는사람
공부처음하는사람
  • 공부처음하는사람
    lazzzykim
    공부처음하는사람
  • 전체
    오늘
    어제
    • 분류 전체보기 (159)
      • Kotlin (31)
      • Java (56)
      • Spring (44)
      • JPA (6)
      • Algorithm (3)
      • TroubleShooting (1)
      • 내일배움캠프 프로젝트 (14)
      • Setting (2)
      • ... (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 글쓰기
  • 링크

  • 인기 글

  • 태그

    java
    빈 생명주기
    spring
    다형성
    제네릭
    kotlin
    예외처리
    배열
    김영한의 실전 자바
    트랜잭션
    언체크예외
    내일배움캠프
    싱글톤
    중첩클래스
    캡슐화
    김영한
    jpa
    OCP
    김영한의 실전자바
    Di
  • hELLO· Designed By정상우.v4.10.3
공부처음하는사람
데이터 접근 기술 - 스프링 JdbcTemplate
상단으로

티스토리툴바