실무에서 주로 사용하는 데이터 접근 기술을 알아보자
크게 2가지 분류가 있다.
SQL Mappter
JDBC Template
MyBatis
주요 기능
ORM 관련 기술
JPA (+ Spring Data JPA) + Querydsl
주요기능
강의는 위의 기술들을 전부 한번 간단하게 설명을 해주었다. 하지만 이곳에서는 JBDC Template만 정리하겠다.
Mybatis는 너무 쉽다. 진짜 사용 방법만 보고, 사용하면 바로 사용이 가능한 수준.
반대로 JPA는 너무 양이 방대하다. 따로JPA만 정리해서 올릴 예정이기때문에 여기선 다루지 않겠다.
따라서 JDBC Template을 위주로 알아보고 Mybatis는 따로 정리하지 않겠다.
들어가기전... 참조
DTO(data transfer object)
도메인을 바로 사용하기엔 너무
데이터 전송 객체. DTO는 기능은 없고 데이터를 전달만 하는 용도로 사용되는 객체를 뜻한다.
참고로 DTO에 기능이 있으면 안되는가? 그것은 아니다. 객체의 주 목적이 데이터를 전송하는
것이라면 DTO라 할 수 있다 객체 이름에 DTO를 꼭 붙여야 하는 것은 아니다. 대신 붙여두면 용도를 알 수 있다는 장점은 있다.
이전에 설명한 ItemSearchCond 도 DTO 역할을 하지만, 이 프로젝트에서 Cond 는 검색 조건으로
사용한다는 규칙을 정했다. 따라서 DTO를 붙이지 않아도 된다. ItemSearchCondDto 이렇게 하면 너무 복잡해진다. 그리고 Cond 라는 것만 봐도 용도를 알 수 있다.
참고로 이런 규칙은 정해진 것이 없기 때문에 해당 프로젝트 안에서 일관성 있게 규칙을 정하면 된다
spring.profiels.active
/src/main/resources 하위의 application.properties
이 프로필은 로컬, 운영 환경, 테스트 실행 등등 다양한 환경에 따라서 다른 설정을 할 때 사용하는 정보이다.
/src/test/resources 하위의 application.properties
test의 profiels도 따로 분리 가능하다. 테스트 코드를 사용할때 이 프로필을 적용 시키는 것이다.
SQL을 직접 사용하는 경우, Spring이 제공하는 JDBC Template은 아주 좋은 선택지이다. JDBC를 편리하게 사용할 수 있게 도와준다.
장점
설정의 편리함
Spring-jdbc
라이브러리에 포함되어 있는데, 스프링으로 JDBC를 사용할 떄 기본적으로 사용되는 라이브러리이다. 별도의 복잡한 설정없이 라이브러리 추가만 해주면 바로 사용 가능.
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
반복 문제 해결 가능
JDBC Template은 콜백 패턴을 사용해서, JDBC를 직접 사용할 때 발생하는 대부분의 반복 작업을 대신 처리해준다. SQL을 작성하고, 파라미터를 정의, 응답 값을 매핑하면 된다.
단점
동적 SQL을 해결하기 어렵다.
예제
private final JdbcTemplate template;
public JdbcTemplateItemRepositoryV1(DataSource dataSource) {
this.template = new JdbcTemplate(dataSource);
}
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 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;
};
}
JDBC 템플릿도 시간이 지나면서 점점 발전해왔다. 앞으로 더 발전한 버전을 서서히 보자.
JdbcTemplate을 기본으로 사용하면 파라미터를 순서대로 바인딩 한다.
String sql = "update item set item_name=?, price=?, quantity=? where id=?";
template.update(sql,
itemName,
price,
quantity,
itemId);
여기서는 itemName , price , quantity 가 SQL에 있는 ? 에 순서대로 바인딩 된다.
따라서 순서만 잘 지키면 문제가 될 것은 없다. 그런데 문제는 변경시점에 발생한다
만약 sql에 price
와 quantity
의 순서가 바뀐다면?
이런 일이 안일어 날 거 같지만 실무에서는 파라미터가 10~20개가 넘어가는 일도 아주 많다. 그래서 미래에 필드를 추가하거나, 수정하면서 이런 문제가 충분히 발생할 수 있다.
JdbcTemplate은 이런 문제를 보완하기 위해 NamedParameterJdbcTemplate
라는 이름을 지정해서 파라미터를 바인딩 하는 기능을 제공한다
코드 예시
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;
}
private RowMapper<Item> itemRowMapper() {
return BeanPropertyRowMapper.newInstance(Item.class); //camel 변환 지원
}
BeanPropertySqlParameterSource
가 item을 받아서 자동으로 매칭시켜 준다. 이렇게된다면 이름으로 매칭시켜 주기 때문에 순서와는 상관없어 진다.이 파라미터 매칭에서는 3가지 방식으로 가능하다
Map<String, Object> param = Map.of("id", id);
Item item = template.queryForObject(sql, param, itemRowMapper());
Map 과 유사한데, SQL 타입을 지정할 수 있는 등 SQL에 좀 더 특화된 기능을 제공한다. SqlParameterSource 인터페이스의 구현체이다. MapSqlParameterSource 는 메서드 체인을 통해 편리한 사용법도 제공한다.
SqlParameterSource param = new MapSqlParameterSource()
.addValue("itemName", updateParam.getItemName())
.addValue("price", updateParam.getPrice())
.addValue("quantity", updateParam.getQuantity())
.addValue("id", itemId); //이 부분이 별도로 필요하다.
template.update(sql, param);
자바빈 프로퍼티 규약을 통해서 자동으로 파라미터 객체를 생성한다.
예) ( getXxx() -> xxx, getItemName() -> itemName )
key=itemName, value=상품명 값
key=price, value=가격 값
SqlParameterSource 인터페이스의 구현체이다.
SqlParameterSource param = new BeanPropertySqlParameterSource(item);
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(sql, param, keyHolder);
여기서 보면 BeanPropertySqlParameterSource 가 많은 것을 자동화 해주기 때문에 가장 좋아보이지만, BeanPropertySqlParameterSource 를 항상 사용할 수 있는 것은 아니다. 예를 들어서 update() 에서는 SQL에 :id 를 바인딩 해야 하는데, update() 에서 사용하는 ItemUpdateDto 에는 itemId 가 없다. 따라서 BeanPropertySqlParameterSource 를 사용할 수 없고, 대신에 MapSqlParameterSource 를 사용했다.
BeanPropertyRowMapper
Version 1 - itemRowMapper()
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;
};
}
Version 2 - itemRowMapper()
private RowMapper<Item> itemRowMapper() {
return BeanPropertyRowMapper.newInstance(Item.class); //camel 변환 지원
}
BeanPropertyRowMapper
가 자바빈 프로퍼티 규약에 맞춰 데이터를 변환한다. 다만 item_name이라는 이름의 객체는 item에 없는데 어떻게 프로퍼티를 찾아서 변환해주는 것일까?
자바 객체는 카멜( camelCase ) 표기법을 사용한다. itemName 처럼 중간에 낙타 봉이 올라와 있는 표기법이다. 반면에 관계형 데이터베이스에서는 주로 언더스코어를 사용하는 snake_case 표기법을 사용한다.
item_name 처럼 중간에 언더스코어를 사용하는 표기법이다. 이 부분을 관례로 많이 사용하다 보니 BeanPropertyRowMapper 는 언더스코어 표기법을 카멜로 자동 변환해준다.
JdbcTemplate은 INSERT SQL를 직접 작성하지 않아도 되도록 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"); //생략 가능
}
@Override
public Item save(Item item) {
SqlParameterSource param = new BeanPropertySqlParameterSource(item);
Number key = jdbcInsert.executeAndReturnKey(param);
item.setId(key.longValue());
return item;
}
단건 조회 - 숫자 조회
int rowCount = jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);
하나의 로우를 조회할 때는 queryForObject() 를 사용하면 된다. 지금처럼 조회 대상이 객체가 아니라 단순 데이터 하나라면 타입을 Integer.class , String.class 와 같이 지정해주면 된다.
단건 조회 - 객체 조회
Actor actor = jdbcTemplate.queryForObject(
"select first_name, last_name from t_actor where id = ?",
(resultSet, rowNum) -> {
Actor newActor = new Actor();
newActor.setFirstName(resultSet.getString("first_name"));
newActor.setLastName(resultSet.getString("last_name"));
return newActor;
}, 1212L);
객체 하나를 조회한다. 결과를 객체로 매핑해야 하므로 RowMapper
를 사용해야 한다. 여기서는 람다를 사용했다. 따로 RowMapper를 메서드로 빼서 만들어줘도 된다.
목록 조회 - 객체
List<Actor> actors = jdbcTemplate.query(
"select first_name, last_name from t_actor",
(resultSet, rowNum) -> {
Actor actor = new Actor();
actor.setFirstName(resultSet.getString("first_name"));
actor.setLastName(resultSet.getString("last_name"));
return actor;
});
여러 로우를 조회할 때는 query()
를 사용하면 된다. 결과를 리스트로 반환한다.
결과를 객체로 매핑해야 하므로 RowMapper 를 사용해야 한다.
변경(INSERT, UPDATE, DELETE)
등록
jdbcTemplate.update(
"insert into t_actor (first_name, last_name) values (?, ?)",
"Leonor", "Watling");
수정
jdbcTemplate.update(
"update t_actor set last_name = ? where id = ?",
"Banjo", 5276L);
삭제
jdbcTemplate.update(
"delete from t_actor where id = ?",
Long.valueOf(actorId));
데이터를 변경할 때는 jdbcTemplate.update() 를 사용하면 된다. 참고로 int 반환값을 반환하는데, SQL 실행 결과에 영향받은 로우 수를 반환한다.
기타 기능
DDL
jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
임의의 SQL을 실행할 때는 execute() 를 사용하면 된다. 테이블을 생성하는 DDL에 사용할 수 있다.
JdbcTemplate의 최대 단점이 있는데, 바로 동적 쿼리 작성의 어려움을 해결하지 못한다는 점이다. 그리고 SQL을 자바 코드로 작성하기 때문에 휴먼에러에 대한 방지나, SQL 라인이 코드를 넘어갈 때 마다 문자 더하기를 해주어야 하는 단점도 있다.
참조 : 본 글은 김영한님의 스프링 강의를 공부하며 정리한 것이다.