프로젝트 구조 설명3 - 테스트
@Autowired
ItemRepository itemRepository;
@AfterEach
void afterEach() {
//MemoryItemRepository 의 경우 제한적으로 사용
if (itemRepository instanceof MemoryItemRepository) {
((MemoryItemRepository) itemRepository).clearStore();
}
}
- afterEach : 테스트는 서로 영향을 주면 안된다. 따라서 각각의 테스트가 끝나고 나면 저장한 데이터를 제거해야 한다. @AfterEach 는 각각의 테스트의 실행이 끝나는 시점에 호출된다. 여기서는 메모리 저장소를 완전히 삭제해서 다음 테스트에 영향을 주지 않도록 초기화 한다.
- 인터페이스에는 clearStore() 가 없기 때문에 MemoryItemRepository 인 경우에만 다운 케스팅을 해서 데이터를 초기화한다. 실제 DB를 사용하는 경우에는 테스트가 끝난 후에 트랜잭션을 롤백해서 데이터를 초기화 할 수 있다.
@Test
void findItems() {
//given
Item item1 = new Item("itemA-1", 10000, 10);
Item item2 = new Item("itemA-2", 20000, 20);
Item item3 = new Item("itemB-1", 30000, 30);
itemRepository.save(item1);
itemRepository.save(item2);
itemRepository.save(item3);
//둘 다 없음 검증
test(null, null, item1, item2, item3);
test("", null, item1, item2, item3);
//itemName 검증
test("itemA", null, item1, item2);
test("temA", null, item1, item2);
test("itemB", null, item3);
//maxPrice 검증
test(null, 10000, item1);
//둘 다 있음 검증
test("itemA", 10000, item1);
}
void test(String itemName, Integer maxPrice, Item... items) {
List<Item> result = itemRepository.findAll(new ItemSearchCond(itemName, maxPrice));
assertThat(result).containsExactly(items);
}
- findItems() 상품을 찾는 테스트이다. 상품명과 상품 가격 조건을 다양하게 비교하는 것을 확인할 수 있다.
- 문자의 경우 null 조건도 있지만, 빈 문자( "" )의 경우에도 잘 동작하는지 검증한다. ( ObjectUtils.isEmpty() )
- like 검색을 사용하기 때문에 부분 문자도 검색된다.
- containsExactly() 는 객체의 순서까지 정확히 맞아야 true 를 반환한다.
참고: 인터페이스를 테스트하자
여기서는 MemoryItemRepository 구현체를 테스트 하는 것이 아니라 ItemRepository 인터페이스를 테스트하는 것을 확인할 수 있다.
@Autowired ItemRepository itemRepository;
인터페이스를 대상으로 테스트하면 향후 다른 구현체로 변경되었을 때 해당 구현체가 잘 동작하는지 같은 테스트로 편리하게 검증할 수 있다.
데이터베이스 테이블 생성
H2 데이터베이스에 접근해서 item 테이블을 생성하자.
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)
);
generated by default as identity
- identity 전략 사용: 기본 키 생성을 데이터베이스에 위임한다. MySQL의 Auto Increment와 같은 방법이다.
- PK인 id 는 개발자가 직접 지정하는 것이 아니라 비워두고 저장하면 데이터베이스가 순서대로 증가하는 값을 사용해서 넣어준다. ex) insert into item(item_name, price, quantity) values ('ItemTest', 10000, 10)
권장하는 식별자 선택 전략
데이터베이스 기본 키 조건
- null 값은 허용하지 않는다.
- 유일해야 한다.
- 변해선 안 된다.
테이블의 기본 키 전략
자연 키(natural key)
- 비즈니스에 의미가 있는 키
- 예: 주민등록번호, 이메일, 전화번호
대리 키(surrogate key)
- 비즈니스와 관련 없는 임의로 만들어진 키, 대체 키로도 불린다.
- 예: 오라클 시퀀스, auto_increment, identity, 키생성 테이블 사용
기본 키로 자연 키보다는 대리 키를 권장한다
자연 키의 대부분은 현재는 유일할 수 있더라도 이후에 변경될 가능성이 있기 때문이다.
비즈니스 환경은 언젠가 변한다
회원 테이블에 주민등록번호가 기본 키로 잡혀 있었다. 문제는 정부 정책이 변경되면서 법적으로 주민등록번호를 저장할 수 없게 되면서 발생했다. 결국 데이터베이스 테이블은 물론이고 수많은 애플리케이션 로직을 수정해야 한다.
기본 키의 조건을 현재는 물론이고 미래까지 충족하는 자연 키를 찾기는 쉽지 않다. 대리 키는 비즈니스와 무관한 임의의 값이므로 요구사항이 변경되어도 기본 키가 변경되는 일은 드물다. 대리 키를 기본 키로 사용하되 주민등록번호나 이메일처럼 자연 키의 후보가 되는 컬럼들은 필요에 따라 유니크 인덱스를 설정해서 사용하는 것을 권장한다.
참고로 JPA는 모든 엔티티에 일관된 방식으로 대리 키 사용을 권장한다. 비즈니스 요구사항은 계속해서 변하는데 테이블은 한 번 정의하면 변경하기 어렵기 때문이다.
2. 데이터 접근 기술 - 스프링 JdbcTemplate
JdbcTemplate 소개와 설정
SQL을 직접 사용하는 경우에 스프링이 제공하는 JdbcTemplate은 아주 좋은 선택지다.
JdbcTemplate 은 JDBC를 매우 편리하게 사용할 수 있게 도와준다.
장점
설정의 편리함
JdbcTemplate은 spring-jdbc 라이브러리에 포함되어 있는데, 이 라이브러리는 스프링으로 JDBC를 사용할 때 기본으로 사용되는 라이브러리이다. 그리고 별도의 복잡한 설정 없이 바로 사용할 수 있다.
반복 문제 해결
JdbcTemplate은 템플릿 콜백 패턴을 사용해서, JDBC를 직접 사용할 때 발생하는 대부분의 반복 작업을 대신 처리해준다.
개발자는 SQL을 작성하고, 전달할 파리미터를 정의하고, 응답 값을 매핑하기만 하면 된다.
우리가 생각할 수 있는 대부분의 반복 작업을 대신 처리해준다.
- 커넥션 획득
- statement 를 준비하고 실행
- 결과를 반복하도록 루프를 실행
- 커넥션 종료, statement , resultset 종료
- 트랜잭션 다루기 위한 커넥션 동기화
- 예외 발생시 스프링 예외 변환기 실행
단점
동적 SQL을 해결하기 어렵다.
JdbcTemplate 설정
build.gradle
//JdbcTemplate 추가
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
//H2 데이터베이스 추가
runtimeOnly 'com.h2database:h2'
org.springframework.boot:spring-boot-starter-jdbc 를 추가하면 JdbcTemplate이 들어있는 spring-jdbc 가 라이브러리에 포함된다. 별도의 추가 설정은 없다.
H2 데이터베이스에 접속해야 하기 때문에 H2 데이터베이스의 클라이언트 라이브러리(Jdbc Driver)도 추가하자.
JdbcTemplate 적용1 - 기본
JdbcTemplateItemRepositoryV1 클래스
/**
* JdbcTemplate
*/
@Slf4j
@Repository
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 = ?";
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 where id=?";
try{
Item item = template.queryForObject(sql, itemRowMapper(), id);
//쿼리 결과가 없으면 어차피 예외가 발생하기 때문에 ofNullable 로 하지 않음
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 을 생성한다. 스프링에서는 JdbcTemplate 을 사용할 때 관례상 이 방법을 많이 사용한다. 물론 JdbcTemplate 을 스프링 빈으로 직접 등록하고 주입받아도 된다.
save() 데이터를 저장한다.
- template.update() : INSERT , UPDATE , DELETE SQL 은 update() 를 사용한다. 반환 값은 int 인데, 영향 받은 로우 수를 반환한다.
- 데이터를 저장할 때 identity (auto increment) 방식으로 PK 를 생성하기 때문에 PK인 ID 값을 비워두고 저장하면 데이터베이스가 대신 생성해준다. 문제는 애플리케이션 입장에서 데이터베이스에 INSERT가 완료 되어야 생성된 PK ID 값을 확인할 수 있다.
- KeyHolder 와 connection.prepareStatement(sql, new String[]{"id"}) 를 사용해서 id 를 지정해주면 INSERT 쿼리 실행 이후에 데이터베이스에서 생성된 ID 값을 조회할 수 있다. 물론 데이터베이스에서 생성된 ID 값을 조회하는 것은 순수 JDBC로도 가능하지만, 코드가 훨씬 더 복잡하다. 참고로 JdbcTemplate이 제공하는 SimpleJdbcInsert 라는 훨씬 편리한 기능이 있으므로 대략 이렇게 사용한다 정도로만 알아두면 된다.
update() 데이터를 업데이트 한다.
- template.update() : INSERT , UPDATE , DELETE SQL 은 update() 를 사용한다.
- ? 에 바인딩할 파라미터를 순서대로 전달한다.
- 반환 값은 해당 쿼리의 영향을 받은 로우 수 인데, where id=? 를 지정했기 때문에 영향 받은 로우수는 최대 1개이다.
findById() 데이터를 하나 조회한다.
- template.queryForObject() : 결과 로우가 하나일 때 사용한다.
- RowMapper 는 데이터베이스의 반환 결과인 ResultSet 을 제네릭 타입 객체로 변환한다. 이때 파라미터로 넘긴 코드 조각을 사용한다.
- 결과가 없으면 EmptyResultDataAccessException 예외가 발생한다.
- 결과가 둘 이상이면 IncorrectResultSizeDataAccessException 예외가 발생한다.
- ItemRepository.findById() 인터페이스는 결과가 없을 때 Optional 을 반환해야 한다. 따라서 결과가 없을 때 예외를 잡아서 Optional.empty 를 대신 반환하면 된다.
<T> T queryForObject(String sql, RowMapper<T> rowMapper, Object... args) throws DataAccessException;
findAll() 데이터를 리스트로 조회한다. 그리고 검색 조건으로 적절한 데이터를 찾는다.
- template.query() 결과가 하나 이상일 때 사용한다.
- RowMapper 는 데이터베이스의 반환 결과인 ResultSet 을 제네릭 타입 객체로 변환한다.
- 결과가 없으면 빈 컬렉션을 반환한다. 동적 쿼리에 대한 부분은 바로 다음에 다룬다.
<T> List<T> query(String sql, RowMapper<T> rowMapper, Object... args) throws DataAccessException;
itemRowMapper()
- 데이터베이스의 조회 결과를 객체로 변환할 때 사용한다.
- JDBC를 직접 사용할 때 ResultSet 에서 컬럼 값을 조회해서 객체를 초기화 해준 과정과 같다.
- 차이가 있다면 JdbcTemplate이 다음과 같은 루프를 돌려주고, 개발자는 RowMapper 를 구현해서 그 내부 코드만 채운다고 이해하면 된다.
while(resultSet 이 끝날 때 까지) {
rowMapper(rs, rowNum)
}
JdbcTemplate 적용2 - 동적 쿼리 문제
findAll() 에서 사용자가 검색하는 값(파라미터)에 따라서 실행하는 SQL이 동적으로 달려져야 한다.
검색 조건: 상품명, 최대 가격
-> 쿼리의 경우의 수 4
검색 조건이 없음
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을 동적으로 생성해야 한다.
동적 쿼리가 언듯 보면 쉬워 보이지만, 막상 개발해보면 생각보다 다양한 상황을 고민해야 한다.
예를 들어서 어떤 경우에는 where 를 앞에 넣고 어떤 경우에는 and 를 넣어야 하는지 등을 모두 계산해야 한다.
그리고 각 상황에 맞추어 파라미터도 생성해야 한다.
실무에서는 이보다 훨씬 더 복잡한 동적 쿼리들이 사용된다.
참고로 다른 SQL Mapper 인 MyBatis의 가장 큰 장점은 SQL을 직접 사용할 때 동적 쿼리를 쉽게 작성할 수 있다는 점이다.
JdbcTemplate 적용3 - 구성과 실행
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 - 변경
//@Import(MemoryConfig.class)
@Import(JdbcTemplateV1Config.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {}
데이터베이스 접근 설정
src/main/resources/application.properties
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=
스프링 부트가 해당 설정을 사용해서 커넥션 풀과 DataSource , 트랜잭션 매니저를 스프링 빈으로 자동 등록한다.
참고로 서버를 다시 시작할 때 마다 TestDataInit 이 실행되기 때문에 itemA , itemB 도 데이터베이스에 계속 추가된다.
메모리와 다르게 서버가 내려가도 데이터베이스는 유지되기 때문이다
로그 추가
JdbcTemplate이 실행하는 SQL 로그를 확인하려면 application.properties 에 다음을 추가하면 된다.
main , test 설정이 분리되어 있기 때문에 둘다 확인하려면 두 곳에 모두 추가해야 한다.
#jdbcTemplate sql log
logging.level.org.springframework.jdbc=debug
JdbcTemplate - 이름 지정 파라미터 1
순서대로 바인딩
JdbcTemplate을 기본으로 사용하면 파라미터를 순서대로 바인딩 한다.
String sql = "update item set item_name=?, price=?, quantity=? where id=?";
template.update(sql,
itemName,
price,
quantity,
itemId);
itemName , price , quantity 가 SQL에 있는 ? 에 순서대로 바인딩 된다.
그런데 만약 SQL 코드의 컬럼 순서를 변경하면 기존에 바인딩한 파라미터와 맞지 않는 문제가 발생한다.
이럴일이 없을 것 같지만, 실무에서는 파라미터가 10~20개가 넘어가는 일도 아주 많다. 그래서 미래에 필드를 추가하거나, 수정하면서 이런 문제가 충분히 발생할 수 있다.
버그 중에서 가장 고치기 힘든 버그는 데이터베이스에 데이터가 잘못 들어가는 버그다.
이것은 코드만 고치는 수준이 아니라 데이터베이스의 데이터를 복구해야 하기 때문에 버그를 해결하는데 들어가는 리소스가 어마어마하다.
개발을 할 때는 코드를 몇줄 줄이는 편리함도 중요하지만, 모호함을 제거해서 코드를 명확하게 만드는 것이 유지보수 관점에서 매우 중요하다.
이처럼 파라미터를 순서대로 바인딩 하는 것은 편리하기는 하지만, 순서가 맞지 않아서 버그가 발생할 수도 있으므로 주의해서 사용해야 한다.
이름 지정 바인딩
JdbcTemplate은 이런 문제를 보완하기 위해 NamedParameterJdbcTemplate 라는 이름을 지정해서 파라미터를 바인딩 하는 기능을 제공한다.
JdbcTemplateItemRepositoryV2 클래스
/**
* NamedParameterJdbcTemplate
* SqlParameterSource
* - BeanPropertySqlParameterSource
* - MapSqlParameterSource
* Map
*
* BeanPropertyRowMapper
*/
@Slf4j
@Repository
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); //camel 변환 지원
}
}
- JdbcTemplateItemRepositoryV2 은 ItemRepository 인터페이스를 구현했다.
- this.template = new NamedParameterJdbcTemplate(dataSource) : NamedParameterJdbcTemplate 도 데이터소스( dataSource )가 필요하다. 생성자에서 dataSource 를 의존 관계 주입 받고 JdbcTemplate 을 생성한다. 스프링에서는 JdbcTemplate 을 사용할 때 관례상 이 방법을 많이 사용한다. 물론 JdbcTemplate 을 스프링 빈으로 직접 등록하고 주입받아도 된다.
JdbcTemplate - 이름 지정 파라미터 2
SQL에서 ? 에서 :파라미터이름 을 사용하여 바인딩 할 위치를 지정한다.
파라미터를 전달하려면 Map 처럼 key , value 데이터 구조를 만들어서 전달해야 한다.
여기서 key 는 :파라미터이름 으로 지정한 파라미터의 이름이고, value 는 해당 파라미터의 값이 된다.
key-value 구조의 파라미터 데이터를 아래 방식으로 템플릿에 전달한다.
template.update(sql, param, keyHolder);
이름 지정 바인딩에서 자주 사용하는 파라미터의 종류는 크게 3가지가 있다.
1. Map
SqlParameterSource (인터페이스)
2. MapSqlParameterSource
3. BeanPropertySqlParameterSource
1. Map
단순히 Map 을 사용한다. key 와 SQL :파라미터이름 이 매칭되고 value 값으로 치환된다.
String sql = "select id, item_name, price, quantity from item where id=:id";
Map<String, Object> param = Map.of("id", id);
Item item = template.queryForObject(sql, param, itemRowMapper());
2. MapSqlParameterSource
Map 과 유사한데, SQL 타입을 지정할 수 있는 등 SQL에 좀 더 특화된 기능을 제공한다.
SqlParameterSource 인터페이스의 구현체이다.
MapSqlParameterSource 는 메서드 체인을 통해 편리한 사용법도 제공한다.
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);
3. BeanPropertySqlParameterSource
자바빈 프로퍼티 규약(getter)을 통해서 자동으로 파라미터 객체를 생성한다.
예) ( getXxx() -> xxx, getItemName() -> itemName )
예를 들어서 getItemName() , getPrice() 가 있으면 다음과 같은 key-value 데이터를 자동으로 만들어낸다.
key=itemName, value=상품명 값
key=price, value=가격 값
SqlParameterSource 인터페이스의 구현체이다.
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);
BeanPropertySqlParameterSource 를 항상 사용할 수 있는 것은 아니다.
바인딩할 파라미터의 프로퍼티가 하나라도 없을 경우, 즉 같은 이름의 필드가 없으면 사용할 수 없다.
그 예로 update() 에서 SQL에 :id 를 바인딩 해야 하는데, update() 에서 사용하는 ItemUpdateDto 에는 itemId 가 없다. 따라서 BeanPropertySqlParameterSource 를 사용할 수 없고, 대신에 MapSqlParameterSource 를 사용했다.
BeanPropertyRowMapper
JdbcTemplateItemRepositoryV1 - 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;
};
}
JdbcTemplateItemRepositoryV2 - itemRowMapper()
private RowMapper<Item> itemRowMapper() {
return BeanPropertyRowMapper.newInstance(Item.class); //camel 변환 지원
}
BeanPropertyRowMapper 는 ResultSet 의 결과를 받아서 자바빈 규약에 맞추어 데이터를 변환한다.
예를 들어서 데이터베이스에서 조회한 결과가 select id, price 라고 하면 다음과 같은 코드를 작성해준다.
(실제로는 리플렉션 같은 기능을 사용한다.)
Item item = new Item();
item.setId(rs.getLong("id"));
item.setPrice(rs.getInt("price"));
데이터베이스에서 조회한 결과 이름을 기반으로 setId() , setPrice() 처럼 자바빈 프로퍼티 규약에 맞춘 메서드를 호출하는 것이다.
별칭
그런데 select item_name 의 경우 setItem_name() 이라는 메서드가 없다.
이런 경우 개발자가 조회 SQL을 다음과 같이 고치면 된다.
> select item_name as itemName
별칭 as 를 사용해서 SQL 조회 결과의 이름을 변경하는 것이다.
-> 실제 조회는 item_name 으로, 결과는 itemName 으로 출력
실제로 이 방법은 자주 사용된다. 특히 데이터베이스 컬럼 이름과 객체 이름이 완전히 다를 때 문제를 해결할 수 있다.
예를 들어서 데이터베이스에는 member_name 이라고 되어 있는데 객체에 username 이라고 되어 있다면 다음과 같이 해결할 수 있다.
> select member_name as username
그러나...
관례의 불일치
자바 객체는 카멜( camelCase ) 표기법을 사용한다. itemName 처럼 중간에 낙타 봉이 올라와 있는 표기법이다.
반면에 관계형 데이터베이스에서는 주로 언더스코어를 사용하는 snake_case 표기법을 사용한다. item_name 처럼 중간에 언더스코어를 사용하는 표기법이다.
이 부분을 관례로 많이 사용하다 보니 BeanPropertyRowMapper 는 언더스코어 표기법을 카멜로 자동 변환해준다.
따라서 별칭없이 select item_name 으로 조회해도 setItemName() 에 문제 없이 값이 들어간다.
따라서 snake_case 는 자동으로 해결되니 그냥 두면 되고, 컬럼 이름과 객체 이름이 완전히 다른 경우에 조회 SQL에서 별칭을 사용하면 된다.