데이터 접근 기술 - JDBC

Single Ko·2023년 6월 7일
0

Spring 강의 정리

목록 보기
26/31

실무에서 주로 사용하는 데이터 접근 기술을 알아보자

  1. JDBC Template
  2. MyBatis
  3. JPA (+ Spring Data JPA) + QueryDsl

크게 2가지 분류가 있다.

SQL Mappter
JDBC Template
MyBatis

주요 기능

  • SQL만 작성하면 해당 SQL의 결과를 객체로 편리하게 매핑해준다.
  • JDBC를 직접 사용할 때 발생하는 여러가지 중복을 제거해주고, 기타 개발자에게 여러가지 편리한 기능을 제공한다.

ORM 관련 기술
JPA (+ Spring Data JPA) + Querydsl

주요기능

  • 기본적인 SQL은 JPA가 대신 작성하고 처리해준다. 개발자는 저장하고 싶은 객체를 마치 자바 컬렉션에 저장하고 조회하듯이 사용하면 ORM 기술이 데이터베이스에 해당 객체를 저장하고 조회한다.
  • JPA는 Java의 ORM 표준, Hibernate는 JPA에서 가장 많이 사용하는 구현체. 자바에서 ORM을 사용할때는 JPA 인터페이스를 사용하고, 그 구현체로 하이버네이트를 사용한다.
  • Spring Data JPA 와 Querydsl은 JPA를 더 편리하게 사용할 수 있게 도와주는 라이브러리이다.

강의는 위의 기술들을 전부 한번 간단하게 설명을 해주었다. 하지만 이곳에서는 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도 따로 분리 가능하다. 테스트 코드를 사용할때 이 프로필을 적용 시키는 것이다.

JDBC Template

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;
	};
}
  • 얼핏 봐도 동적 쿼리를 짜는데 꽤나 혼란스럽다. 조건문으로 분기가 이리저리 나뉘어 있다.
  • 어떤 경우에 where 앞에 넣고, 어떤 경우에는 and를 넣어야 하는지 등을 모두 계산해야 한다.
  • 각 상황에 맞추어 파라미터도 생성해야 한다.

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에 pricequantity의 순서가 바뀐다면?

이런 일이 안일어 날 거 같지만 실무에서는 파라미터가 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 변환 지원
 }
  • :itemName, :price, :quantity로 받고, BeanPropertySqlParameterSource가 item을 받아서 자동으로 매칭시켜 준다. 이렇게된다면 이름으로 매칭시켜 주기 때문에 순서와는 상관없어 진다.

이 파라미터 매칭에서는 3가지 방식으로 가능하다

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

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);
  1. BeanPropertySqlParameterSource

자바빈 프로퍼티 규약을 통해서 자동으로 파라미터 객체를 생성한다.
예) ( 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 는 언더스코어 표기법을 카멜로 자동 변환해준다.

SimpleJDBC Insert

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;
 }
  • JdbcTemplate : 순서 기반 파라미터 바인딩을 지원한다.
  • NamedParameterJdbcTemplate : 이름 기반 파라미터 바인딩을 지원한다. (권장)
  • SimpleJdbcInsert : INSERT SQL을 편리하게 사용할 수 있다.
  • SimpleJdbcCall : 스토어드 프로시저를 편리하게 호출할 수 있다.

정리

단건 조회 - 숫자 조회

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 라인이 코드를 넘어갈 때 마다 문자 더하기를 해주어야 하는 단점도 있다.

참조 : 본 글은 김영한님의 스프링 강의를 공부하며 정리한 것이다.

profile
공부 정리 블로그

0개의 댓글