본문 바로가기
spring

스프링 DB 2편 - 1

by 오우지 2022. 12. 21.

JdbcTemplate

기본기능

- JDBC를 사용할 때 템플릿 콜백 패턴을 사용해 직접 사용할 때 발생하는 반복 작업을 대신 처리해준다.

- 동적 SQL을 해결하기 어렵다.

 

private final JdbcTemplate template;

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

관례적으로 JdbcTemplate은 dataSource가 필요한데 관례적으로 JdbcTemplate은 생성자 주입을 통해 생성한다.

 

save()

@Override
public Item save(Item item) {
    String sql = "insert into item(item_name, price, quantity) values (?,?,?)";
    KeyHolder keyHolder = new GeneratedKeyHolder();
    int update = 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;
}

데이터를 변경할 때는 update를 사용, 영향받은 row 수를 반환한다. 만약 pk가 autoIncrement라면 개발자가 직접 넣어줄 수 없다. 따라서 pk를 확인하기 위해서는 keyHolder와

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

를 지정해주면 생성된 ID 값을 확인할 수 있다.

 

findById()

template.queryForObject()

결과가 하나일 때 ResultSet을 객체로 반환한다.

결과가 없으면 EmptyResultDataAccessException, 둘 이상이면 IncorrectResultSizeDataAccessException이 발생한다.

 

findAll()

template.query()

결과가 하나 이상일 때 사용

RowMapper는 ResultSet을 객체로 반환한다.

 

findAll()에서는 동적 쿼리를 작성해줘야 하는데 이를 자바 코드로 어지럽게 작성해줘야 한다. 갑자기 MyBatis가 선녀로 보인다.

 

update()

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

여기서는 파라미터가 순서대로 바인딩 되는데 변경 시점에 순서가 바뀌면 다른 값들이 들어가게 된다. 특히나 컬럼이 모두 VARCHAR에 길이도 문제가 없다면 망한다.

 

초기엔 이렇게 사용했지만 JdbcTemplate도 진화했다. NamedParameterJdbcTemplate이라는 이름을 지정해서 파라미터를 바인딩 하는 기능을 제공한다.

 


이름 지정 바인딩

@Slf4j
public class JdbcTemplateItemRepositoryV2 implements ItemRepository {

//    private final JdbcTemplate template;
    private final NamedParameterJdbcTemplate template;

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

JdbcTemplate이 아닌 NamedParameterJdbcTemplate을 사용한다.

save()

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

 

바로 위의 update()에서는 불안정한 순서를 통해 파라미터를 가져왔는데 이름 지정 파라미터에서는 세 가지 방법을 제공한다.

 

1. Map

2. SqlParameterSource

3. BeanPropertySqlParameterSource

 

findById()

이놈은 Map을 사용한다. 원래는 queryForObject를 이용해 Object를 생성해 박아줬다면

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

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

 

여기선 Map을 생성해서 박아준다.

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

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

SqlParameterSource는

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

value의 key를 지정해서 넣어줄 수 있다.

 

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

 

객체에 get이 구현되어 있으면 자동으로 Item을 생성해준다.

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

SimpleJdbcInsert

JdbcTemplate은 Insert SQL을 직접 작성하지 않아도 된다.

 

    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");

    }

SimpleJdbcInsert에 Table 정보를 넣어주면 알아서 메타 정보를 조회해 컬럼 정보를 가져온다.

 

그럼 save()를 간단하게 바꿔줄 수 있다.

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

 

 


테스트 데이터의 분리

지금까지 사용했던 테스트 코드들은 두 번 실행하면 이전 데이터들이 남아 테스트 케이스를 통과하지 못했다. 이건 간단하게 커밋과 롤백을 사용해 처리 가능하다. 또한, 테스트용 DB를 따로 만들어서 사용하면 된다.

 

transactionManager는 스프링 부트에서 자동으로 빈으로 등록 해주기 때문에 주입 받으면 된다.

@Autowired
PlatformTransactionManager transactionManager;
TransactionStatus status;

@BeforeEach
void beforeEach() {
    // 트랜잭션 시작
    status = transactionManager.getTransaction(new DefaultTransactionDefinition());
}

@AfterEach
void afterEach() {
    //MemoryItemRepository 의 경우 제한적으로 사용
    if (itemRepository instanceof MemoryItemRepository) {
        ((MemoryItemRepository) itemRepository).clearStore();
    }
    // 트랜잭션 롤백
    transactionManager.rollback(status);
}

각각의 테스트 앞뒤로 트랜잭션을 얻고, 롤백을 해주는 과정을 가진다. 그래도 귀찮다. @Transactional을 사용하자

 

스프링이 제공하는 @Transactional의 기본 동작은 성공시 커밋이다.

하지만 테스트에서 사용하면 테스트가 끝나면 자동으로 롤백하는 기능을 한다. 로직은 동일하게 BeforeEach에서 트랜잭션 시작, AfterEach에서 트랜잭션 롤백을 수행한다. 다만 @Commit을 붙여주면 커밋이 호출되고 @Rollback(value=false)를 사용해도 된다.

 

 

임베디드 모드 DB

H2는 JVM 메모리에 포함해서 실행할 수 있다. DB를 내장해서 실행한다고 해서 임베디드모드라고 한다.

@Bean
@Profile("test")
public DataSource dataSource() {
   log.info("메모리 데이터베이스 초기화");
   DriverManagerDataSource dataSource = new DriverManagerDataSource();
   dataSource.setDriverClassName("org.h2.Driver");
   dataSource.setUrl("jdbc:h2:mem:db;DB_CLOSE_DELAY=-1");
   dataSource.setUsername("sa");
   dataSource.setPassword("");
   return dataSource;
}

어플리케이션에서 테스트 프로필 시에 DataSource를 등록해준다. 이렇게 등록하면 데이터 소스를 임베디드 모드로 빈으로 등록해서 사용할 수 있다.

 

또한, 스프링 부트는 SQL 스크립트를 실행해서 어플 로딩 시점에 DB를 초기화 하는 기능을 제공한다.

src/test/resources/schema.sql에 추가해주면 된다.

drop table if exists item CASCADE;
create table item
(
    id bigint generated by default as identity,
    item_name varchar(10),
    price integer,
    quantity integer,
    primary key (id)
);

 

아니 근데 스프링 부트는 별다른 설정이 없으면 임베디드 DB를 사용한다. 그래서 설정을 빼주면 된다.

이제 마바로 넘어가보자.

 

'spring' 카테고리의 다른 글

스프링 DB 2편 - 3  (0) 2023.01.05
스프링 DB 2편 - 2  (0) 2023.01.03
스프링 DB 1편 - 3  (0) 2022.12.12
스프링 DB 1편 - 2  (0) 2022.11.30
스프링 DB 1편 - 1  (0) 2022.11.02