스프링 DB - 스프링 JdbcTemplate

Heeeoh·2024년 3월 6일
0

스프링 DB

목록 보기
8/9
post-thumbnail

🌿 시작하기 앞서


스프링 부트 2.6.5 버전을 기준으로 작성됨
H2 데이터베이스 Version 2.2.224 (2023-09-17)

SQL을 직접 사용하는 경우 좋은 DB접근 기술이다.


🌱 JdbcTemplate 기본 사용


장점

  • 설정의 편리함
    • JdbcTemplate은 spring-jdbc 라이브러리에 포함 (스프링 기본 사용 라이브러리)
    • 별도의 복잡한 설정 없이 바로 실행 가능
  • 반복 문제 해결
    • 템플릿 콜백 패턴
    • 개발자는 SQL 작성, 전달 파라미터 정의, 응답 값 매핑 까지만 하면 된다.
    • 대부분의 반복 작업 대신 처리
      • 커넥션 획득
      • statement 를 준비 및 실행
      • 결과를 반복하도록 루프 실행
      • 커넥션 종료, statement, resultset 종료
      • 트랜잭션 다루기 위한 커넥션 동기화
      • 예외 발생시 스프링 예외 변환기 실행

단점

  • 동적 SQL 해결이 어려움

JdbcTemplate를 dependencies에 추가

build.gradle

//JdbcTemplate 추가
implementation 'org.springframework.boot:spring-boot-starter-jdbc'

✔️ JdbcTemplate 적용


기본

private final JdbcTemplate template;

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

JdbcTemplatedataSource가 필요
dataSource를 의존 관계 주입을 받고 생성자 내부에서 JdbcTemplate 생성 (관례상 많이 사용)



save

KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(connection -> {
//자동 증가 키
PreparedStatement ps = connection.prepareStatement(sql, new String[] {"id"}); 
	//ps.setXxx로 파라미터 바인딩
	return ps;
	}, keyHolder);
    
long key = keyHolder.getKey().longValue();

PK 생성에 identity (auto increment) 방식을 사용했기에 id 값을 비워두고 저장
하지만 이러면 insert 가 완료되어야 생성된 PK id값을 확인 가능
-> KeyHolderconnection.prepareStatement(sql, new String[] {"id"}) 를 사용해서 id를 지정해주면 insert 쿼리 실행 이후에 DB에서 생성된 id 값 조회 가능


prepareStatement 메서드 살펴보기
// 여러 개가 있지만 여기서 사용된 메서드 
PreparedStatement prepareStatement(String sql, String columnNames[]) throws SQLException;
  1. sql 문
  2. 자동 생성되는 키들의 컬럼 이름


findById()

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

queryForObject()
  • 결과 로우가 하나일 때 사용
  • RowMapper는 데이터베이스의 반환 결과인 ResultSet을 객체로 변환
  • 결과가 없으면 EmptyResultDataAccessException 예외 발생
  • 결과가 둘 이상이면 IncorrectResultSizeDataAccessException 예외 발생

ItemRepository.findById() 인터페이스는 결과가 없을 때 Optional을 반환해야한다.
따라서 결과가 없으면 예외를 잡아서 Optional.empty를 대신 반환


findAll()

@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());
}

데이터를 리스트로 조회, 검색 조건에 따른 데이터 조회
template.query()

  • 결과가 하나 이상일 때 사용
  • RowMapper는 데이터베이스의 반환 결과인 ResultSet을 객체로 변환
  • 결과가 없으면 빈 컬렉션을 반환
  • 동적 쿼리에 대한 부분을 직접 짜줘야함

RowMapper()

JdbcTemplate가 루프를 돌려주고, 개발자는 RowMapper를 구현해서 그 내부 코드만 채운다.

while(resultSet 이 끝날 때 까지) {
	rowMapper(rs, rowNum);
}

✔️ 동적 쿼리 문제


findAll에서 사용자가 검색하는 값에 따라 SQL이 동적으로 달라져야한다.
select id, item_name, price, quantity from item

select id, item_name, price, quantity from item where item_name like concat('%', ?, '%')

...

검색 조건의 개수는
1개의 검색조건이 있다 없다 하여 2가지 경우의 수이고
2개면 2x2
3개면 2x2x2
즉 검색 조건의 수가 n이라 했을 때
SQL문의 경우의 수는 2^n 이 된다.

또한 2가지 이상의 조건이면 and 를 넣어줘야하고 골치아프다.

MyBatis의 가장 큰 장점은 SQL을 직접 사용할 때 동적 쿼리를 쉽게 작성할 수 있다


✔️ 구성과 실행


JdbcTemplateV1Config

@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);
    }
}

ItemServiceApplication

@Slf4j
//@Import(MemoryConfig.class)
@Import(JdbcTemplateV1Config.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {

데이터 베이스 접근 설정

// application.properties              // src/main
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa

스프링 부트가 해당 설정을 사용해서 커넥션 풀과 DataSource, 트랜잭션 매니저를 스프링 빈으로 자동 등록한다.

JdbcTemplate가 실행하는 SQL 로그 확인 방법

 // application.properties
#jdbcTemplate sql log
logging.level.org.springframework.jdbc=debug


📌 이름 지정 파라미터


✔️ 기존 순서대로 바인딩

String sql = "update item set item_name=?, price=?, quantity=? where id=?";
        template.update(sql,
                updateParam.getItemName(),
                updateParam.getPrice(),
                updateParam.getQuantity(),
                itemId);

SQL 코드의 순서 변경시

String sql = "update item set item_name=?, price=?, quantity=? where id=?";
        template.update(sql,
                updateParam.getItemName(),
                updateParam.getQuantity(),
                updateParam.getPrice(),
                itemId);

가격이 수량이 되고 수량이 가격이되어버리는 큰 사고 발생

개발을 할 때는 코드를 몇줄 줄이는 편리함도 중요하지만, 모호함을 제거해서 코드를 명확하게 만드는 것이 유지보수 관점에서 매우 중요


✔️ 이름 지정 바인딩


이러한 문제들을 해결하기위해 이름을 지정해서 파라미터를 바인딩 하는 기능을 제공
NamedParameterJdbcTemplate를 사용

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

NamedParameterJdbcTemplateDataSource가 필요

save

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

	BeanPropertySqlParameterSource param = new BeanPropertySqlParameterSource(item);

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

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

    return item;
}

SQL 에서 ? -> :파라미터이름으로 받는다.

insert into item(item_name, price, quantity) values (:itemName, :price, :quantity)


✔️ 이름 지정 파라미터 전달


이름 지정 파라미터를 전달하려면 Map 처럼 Key, value 데이터 구조를 만들어서 전달해야 한다.
key : :파라미터이름으로 지정한, 파라미터의 이름
value : 해당 파라미터의 값

이름 지정 바인딩에서 자주 사용하는 파라미터 종류는 크게 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

Map과 유사한데 SQL 타입을 지정할 수 있는 등 SQL에 좀 더 특화된 기능 제공
SqlParameterSource 인터페이스의 구현체
메서드 체인을 통해 편리한 사용법도 제공

SqlParameterSource param = new MapSqlParameterSource()
	.addValue("itemName", updateParam.getItemName())
    .addValue("price", updateParam.getPrice())
    // 파라미터 계속 추가
    .addValue("id", itemId); // id를 넣는 부분이 별도로 필요
template.update(sql, param);

3. BeanPropertySqlParameterSource

SqlParameterSource 인터페이스의 구현체
자바빈 프로퍼티 규약을 통해서 자동으로 파라미터 객체를 생성
getXxx() -> xxx, getItemName() -> itemName

ex) getItemName()

  • key=itemNamem value=상품명 값
SqlParameterSource param = new BeanPropertySqlParameterSource(item);
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(sql, param, keyHolder);

매우 간편하다.

여기서 주의할 점은
:id 바인딩을 해주어야하는 상황에서 파라미터에서 넘어온 Dto에 id값이 없다면 사용할 수 없다. 이때는 MapSqlParameterSource를 사용하자



BeanPropertyRowMapper

기존 itemRowMapper 를 사용할 때는 람다식을 사용해서 하나하나 값을 세팅해주었다.

// 기존
private RowMapper<Item> itemRowMapper() {
    return ((rs, rowNum) -> {
        Item item = new Item();
        item.setId(rs.getLong("id"));
		// setter로 item 값 설정 ...
        return item;
    });
}

이번에는 BeanPropertyRowMapper를 사용해서 간편해졌다.

// new
private RowMapper<Item> itemRowMapper() {
    return BeanPropertyRowMapper.newInstance(Item.class); // camel 변환 지원
}

BeanPropertyRowMapperResultSet의 결과를 받아서 자바빈 규약에 맞추어 데이터를 반환

ex) DB 조회 결과가 select id, itemName이라고 한다면 다음과 같은 코드를 작성해준다. (실제로는 리플렉션 같은 기능을 사용)

Item item = new Item();
item.setId(rs.getLong("id");
item.setItemName(rs.getString("itemName");

데이터베이스에서 조회한 결과 이름을 기반으로 setId(), setPrice() 처럼 자바빈 프로퍼티 규약에 맞춘 메서드를 호출



별칭

컬럼 명에 _ 이 중간에 포함되어 있거나
아예 다른 컬럼명이면 어떻게 해야할까?

이때는 as 를 사용하여 별칭을 붙여준다.
select item_name as itemName
select member_name as username

이러한 방법으로 자주 사용한다.


관례의 불일치

자바 객체는 카멜 표기법
관계형 데이터베이스는 스네이크 케이스 표기법(언더스코어 사용)

BeanPropertyRowMapper는 언더스코어 표기법을 카멜로 자동 변환해준다.
select item_name으로 조회 -> setItemName()에 문제 없이 값이 들어간다.

참고
리플렉션이란?
구체적인 클래스 타입을 알지 못해도 그 클래스의 메소드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API



✔️ SimpleJdbcInsert


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"); 생략 가능 심플 jdbc가 item table의 메타 데이터를 읽음

    }
    

SimpleJdbcInsert 도 dataSource를 받는다.

  • withTableName : 데이터를 저장할 테이블 명 지정
  • usingGeneratedKeyColumns : key를 생성하는 PK 컬럼 명을 지정
  • usingColumns : INSERT SQL에 사용할 컬럼을 지정, 특정 값만 저장하고 싶을 때 사용, 모든 컬럼 저장시 생략가능

SimpleJdbcInsert는 생성 시점에 데이터베이스 테이블의 메타 데이터를 조회
어떤 컬럼이 있는지 확인하기에 usingColumns을 생략 가능

save()

jdbcInsert.executeAndReturnKey(param)을 사용해서 INSERT SQL을 실행하고, 생성된 키 값도 매우 편리하게 조회 가능

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

나머지 코드 부분은 기존과 같다.

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


🔅 JdbcTemplate 기능 정리


✔️ 주요 기능

  • JdbcTemplate
    • 순서 기반 파라미터 바인딩 지원
  • NamedParameterJdbcTemplate
    • 이름 기반 파라미터 바인딩을 지원 (권장)
  • SimpleJdbcInsert
    • INSERT SQL을 편리하게 사용 가능
  • SimpleJdbcCall

✔️ 사용법 정리

스프링 JdbcTemplate 사용 방법 공식 메뉴얼



🔖 학습내용 출처

스프링 DB 2편 - 데이터 접근 활용 기술

Reflection이란 / @yeon

profile
열심히 살자

0개의 댓글