Spring에서의 MyBatis

hoyong.eom·2023년 8월 28일
0

스프링

목록 보기
36/59
post-thumbnail

Spring

MyBatis

MyBatis는 JdbcTemplate와 유사한 계열의 SQL Mapper이다.
MyBatis의 강력한 강점으로는 JdbcTemplate에서 불편했던 동적 쿼리의 작성에서 나타난다.
그 뿐문 아니라 SQL을 XML에 편리하게 작성함으로써 SQL을 여러줄 작성할때 공백으로 인해서 주의하지 않아도 된다.

JdbcTemplate

String sql = "update item " +
 "set item_name=:itemName, price=:price, quantity=:quantity " +
 "where id=:id";

MyBatis

<update id="update">
 update item
 set item_name=#{itemName},
 price=#{price},
 quantity=#{quantity}
 where id = #{id}
</update>

다만, JdbcTemplate는 스프링에 내장된 기능이고 별도의 설정없이 사용할 수 있다는 장점이 있다. 반면에 MyBatis는 약간의 설정이 필요하다.

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

MyBatis 설정

mybatis-spring-boot-starter 라이브러리를 사용하면 MyBatis를 스프링과 통합하고 설정도 아주 간단히 할 수 있다.
build.gradle에 의존관계를 추가한다.

//MyBatis 추가
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'

참고
의존관계 뒤에 버전이 붙는 이유는 스프링ㅂ ㅜ트가 버전을 관리해주는 공식 라이브러리가 아닉 때문이다. 스프링 부트가 관리해주는 경우 버전을 붙이지 않아도 최적의 버전을 자동으로 찾아준다.
ex) springboot-start-xxx 이런건 안넣어줘도됨

참고
MyBatis는 이전에는 iBatis로 불려서 패키지 이름에도 iBatis로 되어 있는 경우가 있으니 신경쓰지 않아도 된다.

의존 관계를 추가하면 아래와 같이 라이브러리가 추가 된다.

  • mybatis-spring-boot-starter : MyBatis를 스프링 부트에서 편리하게 사용할 수 있게 시작하는 라이브러리
  • mybatis-spring-boot-autoconfigure : MyBatis와 스프링 부트 설정 라이브러리
  • mybatis-spring : MyBatis와 스프링을 연동하는 라이브러리
  • mybatis : 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 : 마이바티스에서 타입 정보를 사용할 때는 (resultType)패키지 이름을 적어줘야하는데, 여기에 명시하면 패키지 이름을 생략할 수 있다.(전체 패키지 경로가 아닌 단일 패키지 이름만 적어주면 된다.) 그리고 지정한 패키지와 그 하위 패키지가 자동으로 인식된다. 여러 위치를 지정하려면 , 또는 ;로 구분하면 된다.
  • mybatis.configuration.map-underscore-to-camel-case : jdbcTemplate의 BeanPropertRowMapper에서 처럼 언더바를 카멜로 자동 변경해주는 기능을 활성화한다.
  • logging.level.hello.itemservice.repository.mybatis=trace : mybatis에서 실행되는 쿼리 로그를 확인할 수 있다.

관례의 불일치

자바 객체에는 주로 카멜 표기법을 사용한다. itemName처럼 중간에 낙타 봉이 올라와 있는 표기법을 말한다.
반면에 관계형 데이터베이스에서는 주로 언더스코어를 사용하는 snake_case 표기법을 사용한다. item_name 처럼 중간에 언더스코어를 사용하는 표기법이다.
이렇게 관례로 많이 사용하다보니 map-underscore-to-camel-case 기능을 활서오하하면 언더스코어 표기법을 카멜로 자동 변환해준다. 따라서 DB에서 select item_name 으로 조회해도 객체의 itemName(setItemName()) 속성에 값이 정상 입력된다.
정리하면 해당 옵션을 켜면 snake_case는 자동으로 해결되니 그냥 두면 되고, 컬럼 이름과 객체 이름이 완전히 다른 경우에는 SQL에서 별칭을 사용하면 된다.

MyBatis 적용

MyBatis를 사용하기 위해서 작성한 Mapper 클래스는 아래와 같다.

import hello.itemservice.domain.Item;
import hello.itemservice.repository.ItemSearchCond;
import hello.itemservice.repository.ItemUpdateDto;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import java.util.List;
import java.util.Optional;

@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);
}
  • 마이바티스 매핑 XML을 호출해주는 매퍼 인터페이스이다.
  • 이 인터페이스에는 @Mapper 애노테이션을 붙여줘야한다. 그래야 MyBatis에서 인식할 수 있다.
  • 이 인터페이스의 메서드를 호출하면 다음에 보이는 xml의 해당 SQL을 실행하고 결과를 돌려준다.
  • ItemMapper 인터페이스 구현체는 마이바티스 모듈이 알아서 생성해서 스프링빈으로 등록해준다.
  • @Param은 파라미터가 1개인 경우는 생략 가능하지만, 2개 이상인 경우 생략이 불가능하다.
  • 파리미터의 프로퍼티 값을 SQL에서 #{}으로 바인딩 가능하다.

이제 같은 위치에 실행할 SQL이 있는 XML 맵핑 파일을 만들어 주면 된다.
참고로 자바 코드가 아니기 때문에 src/main/resource 하위에 만들되 패키지 위치는 Mapper 인터페이스와 동일해야한다.

src/main/resources/hello/itemservice/repository/mybatis/ItemMapper.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 &lt;= #{maxPrice}
            </if>
        </where>
    </select>

</mapper>
  • namespace : 앞서 만든 매퍼 인터페이스를 지정한다.
  • 경로와 파일 이름에 주의해야한다.
  • xml 파일의 id에는 매퍼 인터페이스의 함수 이름을 지정한다.
  • resultType에 패키징 경로를 모두 적어줘야하지만, mybatis.type-aliases-package=hello.itemservice.domain 설정으로 생략할 수 있다.

참고)
XML 파일을 원하는 위치에 두고 싶으면 application.properties에 아래와 같이 설정하면 된다.
mybatis.mapper-locations=classpath:mapper/*/.xml

이렇게 하면 resouces/mapper를 포함한 그 하위 폴더에 있는 XML을 XML 맵핑 파일로 인식한다. 이 경우 파일 이름은 자유롭게 설정해도 된다.

XML 특수 문자

XML에세너느 데이터 영역에 <,>와 같은 특수문자를 사용할 수 없기 때문에
아래와 같이 대체해서 사용한다.

< : &lt;
> : &gt;
& : &amp;

MyBatis 설정 및 실행

MyBatis가 @Mapper를 보고 구현체를 만들어서 스프링빈에 등록해준다.
특히, MyBatis 모듈이 데이터소스와 트랜잭션 매니저를 알아서 읽어서 사용하므로 따로 주입받을 필요는 없다.

void save(Item item);
<insert id="save" useGeneratedKeys="true" keyProperty="id">
 insert into item (item_name, price, quantity)
 values (#{itemName}, #{price}, #{quantity})
</insert>
  • InsertSQL은 를 사용하면 된다.
  • id에 매퍼 인터페이스에 설정한 메서드 이름을 지정하면 된다. 여기에서는 메서드 이름이 save()이므로 save로 지정하면 된다.
  • 파리미터는 #{}문법을 사용하면 된다. 그리고 매퍼에서 넘긴 객체의 프로퍼티 이름을 적어주면 된다.
  • #{}문법을 사용하면 PreparedStatement를 사용한다. JDBC의 ?를 치환한다고 생각하면 된다.
  • useGeneratedKeys는 데이터베이스가 키를 생성해 주는 IDENTITY 전략일때 사용한다. keyProperty는 생성되는 키의 속성 이름을 지정한다. Insert가 끝나면 item 객체의 id 속성에 생성된 값이 입력된다.
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 SQL은 를 사용하면 된다.
  • 여기서는 파라미터 Long id, ItemUpdateDto updateParam으로 2개이다. 파라미터가 1개만 있으면 @Param을 지정하지 않아도 되지만, 파라미터가 2개 이상이면 @Param으로 이름을 지정해서 파라미터를 구분해야한다.
Optional<Item> findById(Long id);
  
<select id="findById" resultType="Item">
 select id, item_name, price, quantity
 from item
 where id = #{id}
</select>
  • Select SQL은 <select>를 사용하면 된다.
  • resultType은 반환 타입을 명시하면 된다. 여기서는 결과를 Item객체에 맵핑한다.
    - application.properties에 mybatis.typealiases.package=hello.itemservice.domain 속성을 지정한 덕분에 모든 패키지 명을 다 적지는 않아도 된다. 그렇지 않으면 모든 패키지 명을 다 적어야 한다.
    - jdbcTemplate의 BeanPropertyRowMapper처럼 SELECT SQL의 결과를 편리하게 객체로 바로 변환해준다.
    - mybatis.configuration.map-underscore-to-camel-case=true 속성을 지정한 덕분에 언더스코어를 카멜 표기법으로 자동으로 처리해준다.(item_name -> itemName)
  • 자바 코드에서 반환 객체가 하나이면 Item, Optional과 같이 사용하면 되고, 반환 객체가 하나 이상이면 컬렉션을 사용하며 주로 List를 사용한다.
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 &lt;= #{maxPrice}
 </if>
 </where>
</select>
  • MyBatis는 , 같은 동적 쿼리 문법을 통해 편리한 동적 쿼리를 지원한다.
  • 는 해당 조건이 만족하면 구문을 추가한다.
  • 은 적절하게 where문장 만들어준다.
    - 가 모두 실패하게 되면 SQL where을 만들지 않는다.
    - 가 하나라도 성공하면 처음 나타는 and를 where로 변환해준다.

MyBatis 분석

MyBatis 스프링 연동 모듈에서 구현체를 자동으로 생성해서 스프링 빈으로 등록해준다. 따라서 개발자는 Mapper 인터페이스만 생성해서 SQL 파일과 연결해주면 된다.

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

매퍼 구현체

  • 마이바티스 스프링 연동 모듈이 만들어주는 ItemMapper 의 구현체 덕분에 인터페이스 만으로 편리하게 XML의 데이터를 찾아서 호출할 수 있다.
  • 원래 마이바티스를 사용하려면 더 번잡한 코드를 거쳐야 하는데, 이런 부분을 인터페이스 하나로 매우 깔끔하고 편리하게 사용할 수 있다.
  • 매퍼 구현체는 예외 변환까지 처리해준다. MyBatis에서 발생한 예외를 스프링 예외 추상화인 DataAccessException 에 맞게 변환해서 반환해준다. JdbcTemplate이 제공하는 예외 변환 기능을 여기서도 제공한다고 이해하면 된다

정리

  • 매퍼 구현체 덕분에 마이바티스를 스프링에 편리하게 통합해서 사용할 수 있다.
  • 매퍼 구현체를 사용하면 스프링 예외 추상화도 함께 적용된다
  • 마이바티스 스프링 연동 모듈이 많은 부분을 자동으로 설정해주는데, 데이터베이스 커넥션, 트랜잭션과 관련된 기능도 마이바티스와 함께 연동하고 동기화해준다.

MyBatis 기능 정리

참고
MyBatis 공식 메뉴얼: https://mybatis.org/mybatis-3/ko/index.html
MyBatis 스프링 공식 메뉴얼: https://mybatis.org/spring/ko/index.htm

마이바티스에서 동적 쿼리를 위해서 제공되는 기능은 아래와 같다.

// if 
  
<select id="findActiveBlogWithTitleLike"
 resultType="Blog">
 SELECT * FROM BLOG
 WHERE state = ‘ACTIVE’
 <if test="title != null">
 AND title like #{title}
 </if>
</select>
  • 해당 조건에 따라 값을 추가할지 말지 판단한다.
  • 내부 문법은 OGNL을 사용한다.
// choose, when, otherwise
<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>
  • 자바의 swithc 구문과 유사한 구문도 사용할 수 있다.
// trim,where, set
<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>

위 예제는 문장이 모두 만족하지 않을때 발생한다.

SELECT * FROM BLOG
WHERE

그리고 title만 만족할 때도 문제가 발생한다.

SELECT * FROM BLOG
WHERE
AND title like ‘someTitle’

결국 <where>문을 언제 넣어야할지 상황에 따라서 동적으로 달라진다.
<where>를 사용하면 이런 문제를 해결할 수 있다.

<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>
 </where>
</select>

<where>는 문장이 없으면 where을 추가하지 않는다.문장이 있으면 where을 추가한다. 만약 and가 먼저 시작된다면 and를 지운다.

// foreach
<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)와 같은 문장을 쉽게 완성할 수 있다.
  • 파라미터로 List를 전달하면 된다.

MyBatis 기능 정리2 - 기타 기능

XML 대신 애노테이션으로 SQL을 작성할 수 도 있다.

@Select("select id, item_name, price, quantity from item where id=#{id}")
Optional<Item> findById(Long id);
  • @Insert, @Update, @Delete, @Select 기능이 제공된다.
  • 이 경우 XML에는 <select id="findById">~</select>는 제거해야한다.
  • 동적 SQL이 해결되지 않으므로 간단한 경우만 사용하거나 아예 사용하지 말자.

참고

해당 포스팅은 아래의 강의를 공부하여 정리한 내용입니다 .
김영한님의 SpringDB2-MyBatis

0개의 댓글