MyBatis 소개
SQL을 XML에 편리하게 작성할 수 있게 해주는 SQL Mapper다.
기본적으로 JdbcTemplate이 제공하는 대부분의 기능을 제공하며,
동적 쿼리를 매우 편리하게 작성할 수 있다.
MyBatis 설정
build.gradle
에 라이브러리를 추가하자.
스프링 부트 버전이 2.X대라면 2.2.0
버전으로, 3.X대라면 3.0.3
버전으로 추가하면 된다.
//MyBatis 스프링 부트 3.0 추가
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
그런 다음에 application.properties
에 MyBatis에 대한 설정을 추가해주자.
#MyBatis
mybatis.type-aliases-package=hello.itemservice.domain
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.type-aliases-package
는 별칭을 적용하는 패키지를 명시한다.
MyBatis에서는 타입을 사용하려면 hello.itemservice.domain.Item
처럼 패키지까지 모두 적용해줘야 한다.
하지만 해당 속성을 적용하면 Item
처럼 축약해서 사용할 수 있다.
mybatis.configuration.map-underscore-to-camel-case
는 카멜 케이스 적용 여부를 명시한다.
true로 하게 되면 item_id
라는 컬럼명을 자동으로 itemId
로 가져올 수 있게 변환해준다.
MyBatis 적용1 - 기본
매퍼
MyBatis에서는 매퍼(Mapper)가 DB에 접근하는 역할을 한다.
인터페이스로 생성하며 @Mapper
애노테이션을 붙여줘야 한다.
@Mapper
애노테이션을 붙여줘야지 MyBatis에서 해당 인터페이스를 인식할 수 있다.
각 메소드명은 추후 작성할 XML에 있는 각 SQL의 ID와 동일하게 작성한다.
@Mapper
public interface ItemMapper {
void save(Item item);
void update(@Param("id") Long id, @Param("updateParam") ItemUpdateDto updateParam);
Optional<Item> findById(Long id);
List<Item> findAll(ItemSearchCond itemSearch);
}
파라미터를 넘길 때 @Param("xxx")
애노테이션을 사용하는 것을 알 수 있다.
해당 애노테이션을 사용하면 명시한 이름으로 파라미터 바인딩이 된다.
또한 Long이나 String처럼 값만 갖고 있는 경우에는 해당 이름으로만 바인딩된다.
그런데 필드를 가진 객체의 경우에는 “updateParam.price”처럼 필드 접근이 가능하다.
참고로 과거에는 매퍼에 대해서 구현체도 만들어 줬어야 했으나,
MyBatis 3.0 버전부터는 인터페이스만 만들어도 자동으로 매핑되게 바뀌었다.
XML
파일은 기본적으로 src/main/resources/mybatis
폴더에 생성한다.
별도 업무 규칙이 없다면 통상적으로 매퍼 인터페이스의 이름과 동일하게 생성한다.
만약 위치를 변경하고 싶다면 application.properties
에서
mybatis.mapper-locations
속성을 통해 바꿀 수 있다.
mybatis.mapper-locations=classpath:mapper/**/*.xml
처럼 바꾸는 경우가 많다.
<?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>
XML - MyBatis 문서 명시
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
를 통해
해당 문서가 MyBatis용 XML임을 나타낸다.
XML - namespace
매퍼 태그에 보면 namespace
라는 속성이 있다.
해당 속성에 패키지명과 인터페이스명을 명시해서 해당 인터페이스와 XML을 연결한다.
XML - 메소드명과 id
각 쿼리의 id
는 메소드명과 동일하게 작성된다.
자바단에서 MyBatis를 실행하면 실행한 id와 동일한 쿼리를 찾아서 실행하게 된다.
XML - useGeneratedKeys와 keyProperty
insert문에 보면 useGeneratedKeys
속성과 keyProperty
속성이 있다.
useGeneratedKeys
속성은 데이터 등록 시 PK 값을 반환할 지에 대한 여부고,
keyProperty
속성은 PK 값으로 반환되는 컬럼명이다.
즉, JdbcTemplate
에서 GeneratedKeyHolder
를 사용하던 것과 동일하다.
다만 그냥 사용할 수는 없고 DB가 키를 생성해주는 IDENTITY
전략일 때만 사용 가능하다.
그리고 JdbcTemplate
와 다르게 쿼리가 실행되면 해당 객체에 자동으로 값이 설정된다.
예를 들면 Item
이라는 클래스를 통해서 데이터를 등록하고 나면 자동으로 해당 클래스의 id
에 값이 설정되어 있다.
XML - resultType
select문에 보면 resultType
이라는 속성이 있다.
해당 속성에 명시한 자료형의 종류에 따라 MyBatis가 자동으로 값을 설정해준다.
기본적으로는 패키지명도 모두 작성해줘야 하지만, 아까처럼 축약 속성을 명시해준다면 축약이 가능하다.
그리고 쿼리를 보면 “item_name”처럼 스네이크 케이스로 조회를 하지만, 아까 카멜 케이스를 활성화 시켰기 때문에
DB에서 조회했을 때 MyBatis에서 자동으로 “itemName”으로 변환해준다.
XML - 동적 쿼리리
findAll 쿼리를 보면 where
태그와 if
태그가 있는 것을 알 수 있다.
where
태그는 하위에 존재하는 조건문을 자동으로 생성해준다.
만약 하위의 조건문이 존재한다면 항상 WHERE
명령어를 추가해주고,
AND
나 OR
로 시작한다면 WHERE
로 치환해준다.
if
는 하위에 존재하는 조건문을 해당 태그의 test
속성의 결과에 따라 추가해준다.
test
속성에는 하위에 존재하는 조건문을 사용하는 조건을 명시해주면 된다.
그 결과가 true일 경우에 추가된다.
if
의 경우에는 WHERE
절에서 조건문만 추가하는게 아니라 SELECT
나 FROM
처럼 다양한 곳에서 사용할 수 있다.
XML - CDATA
또한 findAll에서 자세히 보면 <=
라는 부분이 있다.
이건 사실 비교를 위해서 <=
연산자를 사용한 것인데,
XML에서는 <
가 태그를 시작하는 문서로 인지하기 때문에 이를 회피하기 위해 변환한 것이다.
다만 눈에 잘 들어오지는 않는 단점이 있다.
그래서 이를 해결하는 방법이 있는데 XML에서 제공하는 CDATA 문법을 사용하면 된다.
and price <= #{maxPrice}
대신에 <![CDATA[ and price <= #{maxPrice} ]]>
처럼 사용하는 방법이다.
그러면 해당 태그 안에 있는 구문은 모두 문자열로 인식되서 특수문자를 사용할 수 있다.
다만 정말로 단순 문자열로 인식되는 것이 때문에 내부에서는 순수 구문만 작성되어야 해서
where
나 if
같은 태그는 작성하면 안 된다.
아니면 그냥 비교 연산자 부분만 <![CDATA[ <= ]]>
처럼 감싸도 무관하다.
각각 장단점이 있으니 필요한 방법을 사용하자.
MyBatis 적용2 - 설정과 실행
실행하는 것 자체는 단순하다.
매퍼가 리포지토리 역할을 대체해서 서비스에서 바로 실행하든,
아니면 중간에 리포지토리를 만들어서 리포지토리에서 매퍼를 호출하게 하고
서비스에서는 리포지토리를 호출하게 하든 크게 상관없다.
중요한 것은 각 계층에서 맡은 역할을 수행하게 하고, 호출하는 것이다.
MyBatis 적용3 - 분석
매퍼 인터페이스는 구현체가 없는 데 어떻게 동작하는 것일까?
그 정답은 MyBatis 스프링 연동 모듈이다.
- 애플리케이션 로딩 시점에 MyBatis 스프링 연동 모듈은
@Mapper
애노테이션이 붙어있는 인터페이스를 찾는다. - 해당 인터페이스가 발견되면 동적 프록시 기술을 사용해서 해당 인터페이스의 구현체를 만든다.
- 생성된 구현체를 스프링 빈으로 등록한다.
- 서비스 단에서 인터페이스를 호출하면 동적 프록시를 통해 생성된 구현체를 실행한다.
MyBatis 기능 정리1- 동적 쿼리
MyBatis는 다양한 동적 쿼리 기능을 제공한다.
if
이전에 사용했던 if문이다.
test
속성에 명시된 조건의 결과가 true면 내부에 명시된 쿼리를 동적으로 생성해준다.
choose + when + otherwise
if는 단일 조건문이지만 choose는 복합 조건문이다.
일반적인 프로그래밍 언어의 switch문을 생각하면 된다.
choose는 단순히 태그만 존재하며 감싸는 역할만 한다.
실제로는 내부에 작성하는 when 태그와 otherwise 태그의 역할이 크다.
when은 if와 동일하게 test
속성의 결과에 따라 내부 구문을 동적으로 생성한다.
일반적인 switch문처럼 test
속성의 결과가 가장 먼저 true인 구문만 생성한다.
otherwise는 switch문의 default와 동일하다.
모든 when절의 test
속성이 false일 경우에 동작한다.
choose문 작성시 when절은 필수지만, otherwise는 필수가 아니다.
where
이전에 사용했던 where문이다.
하위에 존재하는 구문의 종류에 따라 WHERE
명령어를 추가하거나, AND
나 OR
를 제거한다.
trim
간혹 where 태그가 동작하지 않는 경우가 있을 수 있다.
그럴 때는 trim 태그를 사용하면 동일한 효과를 낼 수 있다.
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<!-- 내부 구문 -->
</trim>
foreach
데이터를 조회할 때 IN절을 사용할 때가 많다.
다만 IN절의 경우에는 특성상 파라미터가 여러 개 필요하다.
그럴 때는 foreach를 사용하면 된다.
<foreach collection="list" item="item" index="index" open="ITEM_ID in (" separator="," close=")">
#{item.id}
</foreach>
collection
속성에는 배열이나 리스트로 넘긴 파라미터의 이름을 쓰면 된다.
item
속성에는 배열을 돌면서 사용될 해당 클래스의 별칭을 쓰면 된다. #{item.id}
처럼 접근 가능하다.
index
속성에는 인덱스로 사용될 변수명을 쓰면 된다. 인덱스이기 때문에 0부터 시작한다.
open
속성에는 foreach가 시작되기 전에 작성될 구문을 쓰면 된다.
separator
속성에는 반복문이 돌 때마다 작성될 구분자를 쓰면 된다.
close
속성에는 foreach가 종료되었을 때 작성될 구문을 쓰면 된다.
foreach는 다양한 곳에 쓸 수 있다.
n건 이상의 데이터를 한꺼번에 등록하거나,
조건절에서 쓸 수 있으니 SELECT, UPDATE, DELETE에서도 쓸 수 있다.
공식문서
더 자세한 내용은 공식 문서를 참고하자.
MyBatis 기능 정리2 - 기타 기능
애노테이션으로 쿼리 작성
아래처럼 XML 대신에 애노테이션으로 쿼리를 작성할 수도 있다.
이 경우를 쓸 때는 XMl에 해당 메소드명과 id가 동일한 쿼리가 있으면 안 된다.
@Select("select id, item_name, price, quantity from item where id=#{id}")
Optional<Item> findById(Long id);
@Select
애노테이션 외에도 @Insert
, @Update
, @Delete
가 있다.
다만 동적 쿼리는 제공되지 않기 때문에 정말 간단한 쿼리가 아니면 사용되지 않는다.
문자열 대체
#{파라미터명}
을 사용하면 숫자 3이 들어가도 문자열 취급을 한다.
그런데 간혹 숫자가 들어오면 숫자로 취급하고 싶을 때가 있다.
그럴 때는 ${파라미터명}
을 사용하면 가능하긴 하다.
다만 ${파라미터명}
을 사용하면 SQL 인젝션 공격
을 당할 수 있다.
그러니 아주 특별한 경우가 아니면 사용하면 안 된다.
재사용 가능한 SQL 조각
sql
태그를 사용하면 SQL 코드를 재사용할 수가 있다.
예를 들어 아래와 같은 태그가 있다고 가정해보자.
<sql id="userColumns"> ${alias}.username, ${alias}.id, ${alias}.password </sql>
이 때 만약 t1과 t2라는 2개의 테이블이 있는데 경우에 따라서 둘 중 하나의 테이블을 사용한다고 가정해보자.
그러면 include 태그와 refid 속성을 사용하면 해당 쿼리를 호출할 수 있다.
또한 property 태그를 통해 값을 바인딩 시켜줄 수도 있다.
<include refid="userColumns"><property name="alias" value="t1"/></include>
위의 쿼리가 MyBatis에 의해 파싱되면
t1.username, t1.id, t1.password
와 같이 변한다.
그리고 코드 조각은 다른 코드 조각을 호출할 수 있으며, 넘겨 받은 값은 전파가 가능하다.
만약 A, B, C라는 코드 조각이 있다고 가정해보자,
A는 B를 호출하고, B는 C를 호출한다고 했을 때,
그 때 A가 B를 호출할 때 1과 2라는 값을 넘겨준다면 C에서도 1과 2라는 값을 사용할 수 있다.
결과 매핑
만약 테이블에서는 “user_id”인데 필드명은 “id”라면 카멜 케이스로는 해결이 안 되기 때문에,
보통은 별칭을 사용할 것이다.
물론 ResultMap이라는 것을 사용하면 그러지 않아도 된다.
<resultMap id="userResultMap" type="User">
<id property="id" column="user_id" />
<result property="username" column="user_name"/>
<result property="password" column="hashed_password"/>
</resultMap>
위처럼 resultMap 태그를 선언하고 id에는 해당 resultMap의 고유 이름을 쓰고, type에는 해당하는 클래스를 명시한다.
그 내부에서는 id 태그와 result 태그를 통해 각 컬럼과 필드명을 매핑시켜준다.
property
속성에는 객체의 필드명이, column
속성에는 테이블의 컬럼명이 들어간다.
id 태그는 PK에 해당하는 경우에 사용한다.
그런 다음에 실제 쿼리에서는 resultType
속성 대신에 resultMap
속성으로 해당 resultMap의 id를 명시하면 된다.
단순하게 테이블 하나에만 매핑하는 게 아니라,
테이블 여러 개에다가 조회 결과를 리스트로 받아서 resultMap에 설정하는 등 다양한 방법이 있다.
다만 JPA같은 ORM 방식이면 이런 경우에 특화되어 있어서 최적화가 되어 있지만,
MyBatis는 그렇지 않기 때문에 공수도 많이 들고 최적화도 어렵다.
그러니 필요한 경우에만 신중하게 사용하자.