본 포스팅은 [인프런] 스프링 DB 2편 - 데이터 접근 활용 기술 강의를 바탕으로 공부하고 정리한 글입니다.
데이터 접근 기술
SQL Mapper 기술
- 개발자가 SQL만 작성하면 해당 SQL의 결과를 객체로 편하게 매핑해준다.
- JDBC를 직접 사용할 때 발생하는 여러가지 중복을 제거해주고, 기타 개발자에게 여러 편리한 기능을 제공해준다.
- 예) Jdbctemplate, MyBatis
ORM 관련 기술
- SQL Mapper 기술은 SQL을 개발자가 직접 작성해야 하지만, JPA는 기본적인 SQL을 대신 작성하고 처리해준다.
따라서 개발자는 저장하고 싶은 객체를 마치 자바 컬렉션에 저장하고 조회하듯이 사용하면 ORM 기술이 데이터베이스에 해당 객체를 저장하고 조회해준다. - 예) JPA, Hibernate, 스프링 데이터 JPA, Querydsl
- JPA는 자바 진영의 ORM 표준이고, Hibernate는 JPA에서 가장 많이 사용하는 구현체이다
- 스프링 데이터 JPA, Querydsl은 JPA를 더욱 편리하게 사용할 수 있도록 도와주는 프로젝트이다. 실무에서 JPA를 사용한다면 이 프로젝트로 필수로 함께 사용하는 것이 좋다.
JdbcTemplate
JdbcTemplate은 JDBC를 매우 편리하게 사용할 수 있게 도와준다.
장점
- 설정이 편리하다
- jdbcTemplate은 spring-jdbc 라이브러리에 포함되어 있어 복잡한 설정 없이 바로 사용할 수 있다.
- JDBC의 반복 문제 해결
- JdbcTemplate은 템플릿 콜백 패턴을 사용해 JDBC를 직접 사용할 때 발생하는 반복 작업을 대신 처리해준다.
- 커넥션 획득
- statement 준비 및 실행
- 결과를 반복하도록 루프 실행
- 커넥션 종료, 리소스 종료
- 트랙잭션을 다루기 위한 커넥션 동기화
- 예외 발생시 스프링 예외 변환기 실행
- 개발자는 SQL을 작성하고, 전달할 파라미터를 정의하고, 응답 값을 매핑해주기만 하면 된다.
- JdbcTemplate은 템플릿 콜백 패턴을 사용해 JDBC를 직접 사용할 때 발생하는 반복 작업을 대신 처리해준다.
단점
- 동적 SQL을 해결하기 어렵다.
JdbcTemplate 사용법
JdbcTemplate에 대한 사용법은 스프링 JdbcTemplate 공식 메뉴얼에 자세히 소개되어 있으므로 참고하자.
단건 조회 - queryForObject()
// int 데이터 조회
int rowCount = jdbcTemplate.queryForObjecct("select count(*) from actor", Integer.class);
// string 데이터 조회
String lastName = jdbcTemplate.queryForObject("select last_name from actor where id = ?", String.class, 100L);
// 객체 조회 (ResultSet을 객체로 매핑하기 위한 RowMapper 필요)
Actor actor = jdbcTemplate.queryForObject("select first_name, last_name form actor where id = ?,
(resultSet, rowNum) -> {
Actor newActor = new Actor();
newActor.setFirstName(resultSet.getString("first_name");
newActor.setLastName(resultSet.getString("last_name");
return newActor;
},
100L);
조회 결과가 1개일 경우 queryForObject()를 사용한다.
- 조회 대상이 단순 데이터일 경우 : 두번째 인자에 타입(Integer.class, String.class)을 지정
- 조회 대상이 객체일 경우 : 결과를 객체로 매핑해야하기 때문에 두번째 인자에 RowMapper를 사용해야 한다. (예제에서는 람다 사용)
목록 조회 - query()
List<Actor> actor = jdbcTemplate.queryForObject("select first_name, last_name form actor where id = ?,
(resultSet, rowNum) -> {
Actor actor = new Actor();
newActor.setFirstName(resultSet.getString("first_name");
newActor.setLastName(resultSet.getString("last_name");
return actor;
});
조회 결과가 여러개일 경우 query()를 사용한다. 사용시 결과를 리스트로 반환한다.
등록, 수정, 삭제 - update()
// 등록
jdbcTemplate.update(
"insert into actor (first_name, last_name) values (?, ?)",
"Leonor", "Watling");
// 수정
jdbcTemplate.update(
"update actor set last_name = ? where id = ?",
"Banjo", 527L);
// 삭제
jdbcTemplate.update(
"delete form actor where id = ?",
Long.valueOf(actorId));
기타 - execute()
jdbcTemplate.execute("create table mytable (id integer, name varchar(100)");
임의의 SQL을 실행하는 경우 execute()를 사용한다. 테이블을 생성하는 DDL에 사용할 수 있다.
JdbcTemplate 기능
- JdbcTemplate : 순서 기반 파라미터 바인딩을 지원 (순서를 잘못 입력할 경우 치명적 오류 발생)
- NamedParameterJdbcTemplate : 이름 기반 파리미터 바인딩 지원 (권장)
- SimpleJdbcInsert : INSERT SQL을 편리하게 사용하도록 제공
- SimpleJdbcCall : 스토어드 프로시저를 편리하게 호출하도록 제공
정리
실무에서 가장 간단하고 실용적인 방법으로 SQL을 사용하려 할때 JdbcTemplate을 사용하면 된다.
또한 JPA와 같은 ORM 기술을 사용하면서 동시에 SQL을 직접 작성해야 할 때 JdbcTemplate을 함께 사용할 수 있다.
하지만 JdbcTemplate는 동적 쿼리 문제를 해결하지 못한다는 단점이 있으며, SQL을 자바 코드로 작성하기 때문에 SQL 라인이 코드를 넘어갈 때 마다 문자 더하기를 해줘야 한다는 단점이 있다.
이러한 동적 쿼리 문제를 해결하면서 SQL도 더욱 편리하게 작성할 수 있도록 도와주는 MyBatis 기술이 있다.
MyBatis
MyBatis는 JdbcTemplate보다 더 많은 기능을 제공하는 SQL Mapper이다.
JdbcTemplate vs MyBatis
JdbcTemplate은 스프링에 내장된 기능이기 때문에 별도의 설정 없이 사용할 수 있지만, MyBaits는 약간의 설정이 필요하다는 단점이 있다.
하지만 MyBatis는 SQL을 XML에 편리하게 작성할 수 있고, 동적쿼리를 매우 편리하게 작성할 수 있다는 매력적인 장점이 있다.
따라서 프로젝트에 동적쿼리와 복잡한 쿼리가 많다면 MyBatis를 사용하고, 단순한 쿼리들이 많다면 JdbcTemplate을 사용하면 된다.
👉🏻 JdbcTemplate - SQL 여러줄
String sql = "update item " +
"set item_name=:itemNmae, price=:price, quantity=:quantity " +
"where id=:id";
👉🏻 MyBatis - SQL 여러줄
<update id="update">
update item
set item_name = #{itemName},
price = #{price},
quantity = #{quantity}
where id = #{id}
</update>
MyBatis 설정(gradle)
👉🏻 의존성 추가
📁 build.gradle
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'
MyBatis는 스프링이 공식적으로 버전을 관리해주지 않아 버전 정보를 추가해줘야 한다.
스프링 부트 3.0 이상을 사용한다면 mybatis-spring-boot-starter:3.0.1을 사용해야 한다.
👉🏻 MyBatis 설정정보 추가
📁 application.properties
# MyBatis
mybatis.type-aliases-package=hello.itemservice.domain
mybatis.configuration.map-underscore-to-camel-case=true
logging.level.hello.itemservice.repository.mybatis=trace
- mybatis.type.aliases-package
- MyBatis에서 타입 정보를 사용할 때 패키지 이름을 적어줘야 하는데, 여기에 명시해주면 패키지 이름 생략이 가능하다.
- 지정한 패키지와 그 하위 패키지가 자동으로 인식된다.
- 여러 위치를 지정하려면 , 또는 ; 로 구분한다.
- mybatis.configuration.map-underscore-to-camel-case=true
- 언더바를 카멜로 자동 변경해주는 기능을 활성화한다.
- 자바 객체 : 카멜 표기법 ex) itemName
- 데이터베이스 : 언더바 표기법 ex) item_name
- 언더바를 카멜로 자동 변경해주는 기능을 활성화한다.
- logging.level.hello.itemservice.repository.mybatis=trace
- Mybatis에서 실행되는 쿼리 로그를 확인할 수 있다.
웹 애플리케이션을 실행하는 main, 테스트를 실행하는 test 각 위치의 application.properties 모두 해당 설정정보를 추가해줘야 한다. (test 위치에 추가해주지 않을 경우 반영되지 않아 정상 실행되지 않는다.)
MyBatis 사용법
MyBatis에 대한 사용법은 MyBatis 공식 메뉴얼에 자세히 소개되어 있으므로 참고하자.
👉🏻 MyBatis 매퍼 인터페이스 생성
📁 java.hello.itemservice.repository.mybatis.ItemMapper
@Mapper
public interface ItemMapper {
void save(Item item);
void update(@Param("id") Long id, @Param("updateParam") ItemUpdateDto updateParam);
List<Item> findAll(ItemSearchCond itemSearch);
Optional<Item> findById(Long id);
}
먼저 MyBatis 매핑 xml을 호출해주는 매퍼 인터페이스를 생성해준다.
이 인터페이스는 @Mapper 애노테이션을 붙여줘야 MyBatis에서 인식할 수 있다.
이 인터페이스의 메서드를 호출하면 xml의 해당 SQL을 실행하고 그 결과를 반환한다.
👉🏻 XML 매핑 파일 생성
📁 resources.hello.itemservice.repository.mybatis.ItemMapper
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="hello.itemservice.repository.mybatis.ItemMapper">
<insert id="save" useGeneratedKeys="true" keyProperty="id">
insert into item (item_name, price, quantity)
values (#{itemName}, #{price}, #{quantity})
</insert>
<update id="update">
update item
set item_name=#{updateParam.itemName},
price=#{updateParam.price},
quantity=#{updateParam.quantity}
where id = #{id}
</update>
<select id="findById" resultType="Item">
select id, item_name, price, quantity
from item
where id = #{id}
</select>
<select id="findAll" resultType="Item">
select id, item_name, price, quantity
from item
<where>
<if test="itemName != null and itemName != ''">
and item_name like concat('%',#{itemName},'%')
</if>
<if test="maxPrice != null">
and price <= #{maxPrice}
</if>
</where>
</select>
</mapper>
이 파일은 자바 코드가 아니기 때문에 src/main/resources 하위에 만들어야 하며, 꼭 매퍼 인터페이스와 패키지 위치를 맞춰주어야 한다.
namespace로 앞서 만든 매퍼 인터페이스를 지정하면 된다.
만약 XML 파일을 다른 위치에 두고 싶다면?
application.properties에 다음 설정을 추가해주면 된다.
mybatis.mapper-locations=classpath:mapper/**/*.xml
insert
void save(Item item);
<insert id="save" useGeneratedKeys="true" keyProperty="id">
insert into item (item_name, price, quantity)
values (#{itemName}, #{price}, #{quantity})
</insert>
- <insert> 태그 사용
- id : 매퍼 인터페이스에 설정한 메서드 이름을 지정
- 파라미터 : #{} 문법을 사용해 매퍼에서 넘긴 객체의 프로퍼티 이름을 지정
- userGeneratedKeys : 데이터베이스가 키를 생성해주는 IDENTITY 전략일 경우 지정
- keyProperty : 생성되는 키의 속성 이름 지정
update
void update(@Param("id") Long id, @Param("updateParam") ItemUpdateDto updateParam);
<update id="update">
update item
set item_name=#{updateParam.itemName},
price=#{updateParam.price},
quantity=#{updateParam.quantity}
where id = #{id}
</update>
- <update> 태그 사용
- 파라미터가 한개일 경우 @Param을 지정하지 않아도 되지만, 두개 이상일 경우 @Param으로 이름을 지정해서 구분해야 한다.
select
Optional<Item> findById(Long id);
<select id="findById" resultType="Item">
select id, item_name, price, quantity
from item
where id = #{id}
</select>
List<Item> findAll(ItemSearchCond itemSearch);
<select id="findAll" resultType="Item">
select id, item_name, price, quantity
from item
<where>
<if test="itemName != null and itemName != ''">
and item_name like concat('%',#{itemName},'%')
</if>
<if test="maxPrice != null">
and price <= #{maxPrice}
</if>
</where>
</select>
- <select> 태그 사용
- resultType : 반환 타입 명시
- 여기서는 결과를 Item 객체에 매핑한다.
- 원래는 패키지 이름을 모두 적어줘야 하지만, 앞서 application.properties에 관련 속성(mybatis.type-aliases-package)을 지정해주었기 때문에 패키지명을 생략할 수 있다.
- 예) resultType="hello.itemservice.domain.Item" → resultType="Item"
- <where>, <if> 같은 동적 쿼리 문법을 통해 편리한 동적 쿼리 지원
- <if> : 해당 조건 만족 시 구문 추가
- <where> : 적절하게 where 문장을 생성
- < : XML에서는 태그로 < , > 같은 특수 문자를 사용하기 때문에 데이터 영역에는 해당 특수문자를 사용할 수 없어 다음과 같이 표현한다.
- < : <
- > : >
- & : &
MyBatis 동작원리
앞서 만들어준 매퍼 인터페이스(ItemMapper)의 구현체를 만들어주지 않았지만 정상 동작하는 것을 확인할 수 있었다.
인터페이스는 껍데기 역할만 제공해줄 뿐이며 스프링 빈으로 등록될 수도 없다.(스프링 빈에는 객체 인스턴스가 등록)
그런데 어떻게 동작할 수 있는 것일까?
MyBatis 스프링 연동 모듈에서 이런 부분을 자동으로 처리해준다.
- 애플리케이션 로딩 시점에 MyBatis 스프링 연동 모듈은 @Mapper 애노테이션이 붙어있는 인터페이스를 조회한다.
- 매퍼 인터페이스를 찾으면 동적 프록시 기술을 사용해 매퍼 인터페이스(ItemMapper)의 구현체를 만든다.
- 생성된 구현체를 기반으로 스프링 빈에 등록한다.
MyBatis 스프링 연동 모듈을 통해 생성된 동적 프록시 객체를 스프링 빈으로 등록해 사용하는 것이다.
다음과 같이 ItemMapper의 클래스를 로그로 찍어보면 실제로 프록시 기술이 사용되었음을 확인할 수 있다.
MyBatis 스프링 연동 모듈이 자동으로 등록해주는 부분은 MyBatisAutoConfiguration 클래스를 참고하자.
정리
- MyBatis 스프링 연동 모듈이 만들어주는 매퍼 구현체 덕분에 인터페이스 만으로 편리하게 XML의 데이터를 찾아 호출할 수 있다.
- 매퍼 구현체를 사용하면 스플이 예외 추상화도 함께 적용된다.
- MyBatis에서 발생한 예외를 스프링 예외 추상화인 DataAccessException에 맞게 변환해서 반환
- MyBatis 스프링 연동 모듈이 많은 부분을 자동으로 설정해주는데, 데이터베이스 커넥션, 트랜잭션과 관련된 기능도 MyBatis와 함께 연동하고 동기화해준다.
MyBatis 기능
MyBatis에는 여러 기능이 있는데 여기서는 간단하게 소개 정도만 한다. 자세한 내용은 공식문서 링크를 통해 확인해보자.
- 동적 SQL 처리 기능 (if, foreeach, choose, trim)
- 애노테이션으로 SQL 작성 기능
- XML 대신 애노테이션에 SQL을 작성할 수 있다.
- @Insert, @Update, @Delete, @Select 기능을 제공한다.
- 동적 SQL이 해결되지 않으므로 간단한 경우에만 사용한다.
@Select("select id, item_name, price, quantity from item where id=#{id}")
Optional<Item> findById(Long id);
- 문자열 대체 기능
- #{} : ?로 파라미터 바인딩 처리
- ${} : 문자 그대로를 처리
@Select("select * from user where ${column} = #{value}")
User findByColumn(@Param("column") String column, @Param("value") String value);
${} 사용 시 SQL 인젝션 공격의 위험이 있어 가급적 사용하면 안된다. 사용하더라고 매우 주의깊에 사용해야 한다.
- 재사용 가능한 SQL 조각
- <sql>을 사용하면 SQL 코드를 재사용 할 수 있다.
- <include>를 통해 <sql> 조각을 찾아서 사용할 수 있다.
'🌱 Spring > DB 접근 기술' 카테고리의 다른 글
트랜잭션 전파 (0) | 2023.06.07 |
---|---|
트랜잭션 이해하기 (0) | 2023.06.01 |
커넥션 풀(Connection Pool) (0) | 2023.03.02 |