Home MyBatis
Post
Cancel

MyBatis

MyBatis

JdbcTemplate하고 비교해 더 많은 기능을 제공하는 SQL Mapper이다. 기본적으로 JDbcTemplate이 제공하는 대부분의 기능을 제공한다.

JdbcTemplate과 비교하면 MyBatis의 가장 매력적인 점은 SQL을 XML에 편리하게 작성할 수 있고, 동적 쿼리를 매우 편리하게 작성할 수 있다는 점이다.

SQL이 여러줄 걸쳐있을 때 JdbcTemplate과의 차이를 보자.

  • JdbcTemlpate
1
2
3
String sql = "update item " +
		"set item_name:itemName, price=:price, quantity=:quantity " +
		"where id=:id";

참고로 공백도 신경써야 한다.

  • MyBatis
1
2
3
4
5
6
7
<update id="update">
	update item
	set item_name=#{itemName},
		price=#{price},
		quantity=#{quantity}
	where id=#{id}
</update>

XML로 작성하기 때문에 라인이 길어지더라도 문자 더하기에 대한 불편함이 없다.

그리고 동적쿼리를 다루는 방법을 비교해보자.

  • JdbcTemplate
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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";
}

JdbcTemplate 에서는 어느 상황에 where가 들어가고, and가 들어가는 지 등등 개발자가 하나하나 고려해서 조건을 달아 처리해주었다. 실수할 확률이 높다.

  • MyBatis
1
2
3
4
5
6
7
8
9
10
11
12
<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 &lt; = #{maxPrice}
		</if>
	</where>
</select>

MyBatis는 동적 쿼리를 매우 편리하게 작성할 수 있는 다양한 기능을 제공해준다.

MyBatis의 단점

JdbcTemplate은 스프링에 내장된 기능이고, 별도의 설정없이 사용이 가능하다.

하지만 MyBatis는 설정이 필요하다.

정리

프로젝트에서 동적 쿼리와 복잡한 쿼리가 많다면 MyBatis를 사용하는 것이 편할 것이고, 단순한 쿼리들이 많다면 JdbcTemplate을 선택해 사용하면 된다.

둘을 함께 사용해도 무방하다.

MyBatis 설정

build.gradle 추가.

1
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.1'

참고로 다른 것과 다르게 뒤에 버전 정보가 붙는데, 이는 스프링 부트가 버전을 관리해주는 공식 라이브러리가 아니기 때문이다. 스프링 부트가 버전을 관리해주는 경우는 버전 정보를 따로 붙이지 않아도 최적의 버전을 자동으로 매치해준다.

스프링부트 3 이상부터 3.0.1을 사용하면 되고, 아래는 2.2.0 버전을 사용하면 된다. (이 부분에서 오류발생 ㅠ)

application.properties 추가.

1
2
3
4
#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.configuration.map-underscore-to-camel-case
    • JdbcTemplate의 BeanPropertyRowMapper 같이 언더바를 카멜로 자동 변경햊는 기능을 활성화한다.
    • 자바 객체의 카멜 표기법과 관계형데이터베이스의 언더스코어 관례 불일치로 인한 설정 필요.
  • logging.level.hello.itemservice.repository.mybatis=trace
    • MyBatis에서 실행되는 쿼리 로그를 확인하는 용도

mybatis.type-aliases-package를 좀 더 자세히 설명하자면, 위의 MyBatis 예시 코드에서 xml에

1
<select id="findById" resultType="Item">

resultType=”Item” 이라고 되어 있는데, 만약 위 설정을 입력하지 않으면 Item 타입의 패키지명까지 모두 입력해주어야 한다.

MyBatis 기본 적용

Mapper

1
2
3
4
5
6
7
8
9
10
11
@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);  
}
  • 마이바티스 매핑 XML을 호출해주는 매퍼 인터페이스이다.
  • 이 인터페이스에는 @Mapper 애노테이션을 붙여주어야 한다. 그래야 MyBatis에서 인식할 수 있다.
  • 이 인터페이스의 메서드를 호출하면 다음에 보이는 xml의 해당 SQL을 실행하고 결과를 돌려준다.

이렇게 매퍼를 만들고, 같은 위치에 실행할 SQL이 있는 XML 매칭 파일을 만들어주면 된다.

참고로 자바 코드가 아니기 때문에 src/main/resources 하위에 만들되, 패키지 위치는 맞춰주어야 한다.

XML

src/main/resources/hello/itemservice/repository/mybatis/ItemMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?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 &lt;= #{maxPrice}  
			</if>  
		</where>  
	</select>  
</mapper>
  • Insert - save()
    • Insert SQL은 insert 태그를 사용한다.
    • id에는 매퍼 인터페이스에 설정한 메서드 이름을 지정하면 된다. (save)
    • 파라미터는 # 문법을 사용한다. 여기는 매퍼 인터페이스에서 넘긴 객체의 프로퍼티 이름을 적어준다. (Item을 넘겼고, Item객체의 프로퍼티)
    • 문법을 사용하면 PreparedStatement를 사용한다. JDBC의 ?를 치환한다.
    • useGeneratedKeys는 자동 키생성인 auto_increment 전략일 때 사용한다.
    • keyProperty는 생성되는 키의 속성 이름을 지정한다.
  • update - update()
    • update SQL은 update 태그를 사용한다.
    • 파라미터가 Long id, ItemUpdateDto updateParam으로 2개가 넘어온다. 파라미터가 이렇게 2개 이상이면 @Param으로 이름을 지정해서 파라미터를 구분해야 한다.
  • select - findById()
    • Select SQL은 select 태그를 사용한다.
    • resultType은 반환 타입을 명시한다. 위의 설정 덕분에 패키지 명을 모두 적지 않아도 된다.
      • JdbcTemplate의 BeanPropertyRowMapper처럼 select SQL의 결과를 편리하게 객체로 바로 바꾸어준다.
      • 설정을 한 덕분에 언더스코어를 카멜 표기법으로 자동 처리해준다. (item_name => itemName)
    • 자바 코드에서 반환 객체가 하나이면 Item, Optional을 사용하면 되고, 반환 객체가 하나이상이면 컬렉션을 사용한다. 주로 List를 사용한다.
  • select - findAll
    • MyBatis는 where, if 태그 같은 동적 쿼리 문법을 통해 편리한 동적 쿼리를 지원한다.
    • if 태그는 해당 조건이 만족하면 구문을 추가한다.
    • where 태그는 적절하게 where 문장을 만들어준다.
      • 위의 코드에서 if 태그가 모두 실패하면 SQL where을 만들지 않는다.
      • 위의 코드에서 if 태그가 하나라도 성공하면 처음 나타나는 and를 where로 변환해준다. => 이런 부분때문에 동적쿼리를 다루는 것이 편리하다.

참고로 XML이라 <= 를 사용하지 못하고 &lt 를 사용한 것을 볼 수 있다.

1
2
3
< : &lt;
> : &gt;
& : &amp;

다른 해결방법으로는 XML에서 지원하는 CDATA 구문 문법을 사용하면 된다. 이 구문 안에서는 특수문자를 사용할 수 있지만 대신 태그가 단순 문자로 인식되기 때문에 if나 where 태그는 사용하지 못한다.

1
2
3
4
5
<if test="maxPrice != null">
	<![CDATA[
	and price <= #{maxPirce}
	]]>
</if>

각각 장단점이 있으므로 필요에 따라 알맞은 방법으로 사용하면 된다.

설정 및 실행

리포지토리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Repository  
@RequiredArgsConstructor  
public class MyBatisItemRepository implements ItemRepository {  
  
	private final ItemMapper itemMapper;  
	  
	@Override  
	public Item save(Item item) {  
		itemMapper.save(item);  
		return item;  
	}  
	  
	@Override  
	public void update(Long itemId, ItemUpdateDto updateParam) {  
		itemMapper.update(itemId, updateParam);  
	}  
	  
	@Override  
	public Optional<Item> findById(Long id) {  
		return itemMapper.findById(id);  
	}  
	  
	@Override  
	public List<Item> findAll(ItemSearchCond cond) {  
		return itemMapper.findAll(cond);  
	}  
}
  • itemMapper를 주입받는다.
  • itemMapper의 구현체는 자동으로 생성해준다.
  • MyBatisRepository는 단순히 ItemMapper에 기능을 위임한다. 코드가 매우 간결해진다.

Config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration  
@RequiredArgsConstructor  
public class MyBatisConfig {  
  
	private final ItemMapper itemMapper;  
	  
	@Bean  
	public ItemService itemService() {  
		return new ItemServiceV1(itemRepository());  
	}  
	  
	@Bean  
	public ItemRepository itemRepository() {  
		return new MyBatisItemRepository(itemMapper);  
	}  
}
  • dataSource가 아닌 itemMapper를 주입받는 MyBatisItemRepository이다.
    • dataSource는 이제 MyBatis 모듈이 DataSource와 TransactionManager 등등을 가져와준다.

이후 설정 적용을 위해 Application에 @Import 해주어야 한다.

작동 방식

ItemMapper 인터페이스의 구현체를 따로 구현해주지 않았는데 주입도 알아서 하고 repository는 의도대로 동작한다.

이 부분은 MyBatis 스프링 연동 모듈에서 자동으로 처리해주는데 아래의 그림과 같다.

  • 애플리케이션 로딩 시점에 MyBatis 스프링 연동 모듈은 @Mapper가 붙은 인터페이스를 조사한다.
  • 해당 인터페이스가 발견되면 동적 프록시 기술을 사용해 ItemMapper 인터페이스의 구현체를 만든다.
  • 생성된 구현체를 스프링 빈으로 등록한다.

이러한 매퍼 구현체는 예외 변환 처리까지 해준다. MyBatis에서 발생한 예외를 스프링 예외 추상화인 DataAccessException에 맞게 변환해 반환해준다.

마이바티스 스프링 연동 모듈이 많은 부분을 자동으로 설정해주는데, 데이터베이스 커넥션, 트랜잭션 매니저 등등 관련 기능도 마이바티스와 함께 연동하고, 동기하해준다.

MyBatisAutoConfiguration 클래스를 확인하면 해당 내용을 볼 수 있다.

MyBatis 기능 정리

동적 쿼리

동적 쿼리를 위해 제공되는 기능은 아래와 같다.

  • if
  • choose (when, otherwise)
  • trim (where, set)
  • foreach

if

1
2
3
4
5
6
7
8
<select id="findActiveBlogWithTitleLike"  
		resultType="Blog">  
	SELECT * FROM BLOG  
	WHERE state = ‘ACTIVE’  
	<if test="title != null">  
		AND title like #{title}  
	</if>  
</select>
  • 해당 조건에 따라 값을 추가할 지 말지 판단한다.
  • 첫 조건이면 and를 where로 변환해준다.
  • 내부의 문법은 OGNL을 사용한다.

choose, when, otherwise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<select id="findActiveBlogLike"  
		resultType="Blog">  
	SELECT * FROM BLOG WHERE state = ‘ACTIVE’  
	<choose>  
		<when test="title != null">  
			AND title like #{title}  
		</when>  
		<when test="author != null and author.name != null">  
			AND author_name like #{author.name}  
		</when>  
		<otherwise>  
			AND featured = 1  
		</otherwise>  
	</choose>  
</select>
  • switch 구문과 비슷하다. choose가 switch이고, when 조건들을 만족하지 않으면 otherwise를 실행한다.

trim, where, set

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<select id="findActiveBlogLike"  
		resultType="Blog">  
	SELECT * FROM BLOG  
	WHERE  
	<if test="state != null">  
		state = #{state}  
	</if>  
	<if test="title != null">  
		AND title like #{title}  
	</if>  
	<if test="author != null and author.name != null">  
		AND author_name like #{author.name}  
	</if>  
</select>

위의 if만 사용한 예시는 문제가 있다. 문장을 모두 만족하지 않을 때 발생한다.

1
2
SELECT * FROM BLOG
WHERE

만족하는 조건이 없으면 이렇게 끝나게 된다.

1
2
3
SELECT * FROM BLOG
WHERE
AND title like 'someTitle'

이러한 문제를 where태그가 처리해준다. 첫 where 조건이면 where를, 그렇지 않으면 and로 변환해준다.

trim도 where 태그와 같은 기능을 한다.

foreach

1
2
3
4
5
6
7
8
9
10
<select id="selectPostIn" resultType="domain.blog.Post">  
	SELECT *  
	FROM POST P  
	<where>  
		<foreach item="item" index="index" collection="list"  
			open="ID in (" separator="," close=")" nullable="true">  
			  #{item}  
		</foreach>  
	</where>  
</select>
  • 컬렉션을 반복 처리할 때 사용한다. where in (1,2,3,4,5,6) 과 같은 문장을 쉽게 완성할 수 있다.
  • 파라미터로 List를 전달하면 된다.

더 자세한 내용

기타 기능

애노테이션으로 SQL 작성

1
2
@Select("select id, item_name, price, quantity from item where id=#{id}")
Optional<Item> findById(Long id);
  • XML 대신 애노테이션에 SQL을 작성할 수도 있다.
  • 단, 동적쿼리를 처리하는 장점이 없어지기 때문에 좋은 선택은 아니다.
  • @Insert, @Update, @Delete, @Select 기능이 제공된다.

문자열 대체

파라미터 바인딩을 사용하지 않고 문자 그대로를 처리하고 싶을 때도 있다. 그럴 때 사용한다.

1
${}
1
2
@Select("select * from user where ${column} = #{value}")
User findByColumn(@Param("column") String column, @Param("value") String value);
  • SQL 인젝션 공격을 당할 수 있으므로 가급적 사용하면 안된다.

재사용 가능한 SQL 조각

1
<sql id="userColumns"> ${alias}.id, ${alias}.username, ${alias}.password </sql>

위와 같이 재사용할 SQL 코드를 정의하고, SQL 코드를 재사용할 때 사용한다.

1
2
3
4
5
6
7
<select id="selectUsers" resultType="map">  
	select  
		<include refid="userColumns"><property name="alias" value="t1"/></include>,  
		<include refid="userColumns"><property name="alias" value="t2"/></include>  
	from some_table t1  
	  cross join some_table t2  
</select>
  • include 태그를 통해 sql 조각을 찾아 사용할 수 있다.
  • select 다음 정의해준 문장을 넣는다.
  • alias의 value가 t1, t2 인 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<sql id="sometable">  
	${prefix}Table  
</sql>  

<sql id="someinclude">  
	from  
		<include refid="${include_target}"/>  
</sql>  

<select id="select" resultType="map">  
	select  
	field1, field2, field3  
	<include refid="someinclude">  
		<property name="prefix" value="Some"/>  
		<property name="include_target" value="sometable"/>  
	</include>  
</select>
  • 프로퍼티 값을 전달할 수 있고, 해당 값은 내부에서 사용할 수 있다.

Result Maps

결과를 매핑할 때 테이블은 user_id 이지만 객체는 id이다.

이 경우 컬럼명과 객체의 프로퍼티 명이 다르다. 그러면 아래와 같이 별칭 as를 사용하면 된다.

1
2
3
4
5
6
7
8
<select id="selectUsers" resultType="User">
	select
		user_id			as "id",
		user_name		as "userName",
		hashed_password	as "hashedPassword"
	from some_table
	where id = #{id}
</select>

별칭을 사용하지 않는 방법이 Result Map이다.

1
2
3
4
5
6
7
8
9
10
11
<resultMap id="userResultMap" type="User">
	<id property="id" column="user_id" />
	<result property="username" column="user_name"/>
	<result property="password" column="hashed_password"/>
</resultMap>

<select id="selectUsers" resultMap="userResultMap">
	select user_id, user_name, hashed_password
	from some_table
	where id=#{id}
</select>

복잡한 결과 매핑

MyBatis도 매우 복잡한 결과에 객체 연관관계를 고려해 데이터를 조회하는 것이 가능하다. (join)

이때는 association, collection 태그 등을 많이 사용한다.

이 부분은 성능과 실효성 측면에서 많은 고민이 필요하다.

JPA는 객체와 관계형 데이터베이스를 ORM 개념으로 매핑하기 때문에 이런 부분이 자연스럽지만 (fetch join 등), MyBatis에서는 들어가는 공수도 많고, 성능을 최적화 하기도 어렵다. 따라서 해당 기능을 사용할 때는 신중하게 사용해야 한다.

해당 기능에 대한 자세한 내용은 공식 메뉴얼을 참고하면 된다.

This post is licensed under CC BY 4.0 by the author.