kinggora 2022. 11. 11. 16:01

4. 데이터 접근 기술 - MyBatis

MyBatis 소개

MyBatis는 앞서 설명한 JdbcTemplate보다 더 많은 기능을 제공하는 SQL Mapper 이다.

기본적으로 JdbcTemplate이 제공하는 대부분의 기능을 제공한다.

JdbcTemplate과 비교해서 MyBatis의 가장 매력적인 점은 SQL을 XML에 편리하게 작성할 수 있고 또 동적 쿼리 문제를 해결할 수 있다는 점이다.

 

JdbcTemplate - SQL 여러줄

String sql = "update item " +
     "set item_name=:itemName, 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는 XML에 작성하기 때문에 라인이 길어져도 문자 더하기에 대한 불편함이 없다.

상품을 검색하는 로직 비교

JdbcTemplate - 동적 쿼리

자바 코드로 SQL 문자열을 직접 조립해야 한다.

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";
   }
log.info("sql={}", sql);
return template.query(sql, param, itemRowMapper());

 

MyBatis - 동적 쿼리

<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>

설정의 장단점

JdbcTemplate은 스프링에 내장된 기능이고, 별도의 설정없이 사용할 수 있다는 장점이 있다.

반면에 MyBatis는 약간의 설정이 필요하다. (최근에는 스프링 부트와 연동하여 편하게 사용할 수 있다)

 

정리

프로젝트에서 동적 쿼리와 복잡한 쿼리가 많다면 MyBatis를 사용하고, 단순한 쿼리들이 많으면 JdbcTemplate을 선택해서 사용하면 된다. 물론 둘을 함께 사용해도 된다. 하지만 MyBatis를 선택했다면 그것으로 충분할 것이다.

 

참고: 강의에서는 MyBatis의 기능을 하나하나를 자세하게 다루지는 않는다. MyBatis를 왜 사용하는지, 그리고 주로 사용하는 기능 위주로 다룰 것이다. 그래도 이 강의를 듣고 나면 MyBatis로 개발을 할 수 있게 되고 추가로 필요한 내용을 공식 사이트에서 찾아서 사용할 수 있게 될 것이다.

MyBatis는 기능도 단순하고 또 공식 사이트가 한글로 잘 번역되어 있어서 원하는 기능을 편리하게 찾아볼 수 있다.

 

MyBatis 공식 사이트: https://mybatis.org/mybatis-3/ko/index.html

MyBatis 설정

mybatis-spring-boot-starter 라이브러리를 사용하면 MyBatis를 스프링과 통합하고, 설정도 아주 간단히 할 수 있다.

의존관계 추가

build.gradle 

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

 

뒤에 버전 정보가 붙는 이유는 스프링 부트가 버전을 관리해주는 공식 라이브러리가 아니기 때문이다.

스프링 부트가 버전을 관리해주는 경우 버전 정보를 붙이지 않아도 최적의 버전을 자동으로 찾아준다.

 

다음과 같은 라이브러리가 추가된다.

  • mybatis-spring-boot-starter : MyBatis를 스프링 부트에서 편리하게 사용할 수 있게 시작하는 라이브러리
  • mybatis-spring-boot-autoconfigure : MyBatis와 스프링 부트 설정 라이브러리
  • mybatis-spring : MyBatis와 스프링을 연동하는 라이브러리
  • mybatis : MyBatis 라이브러리

설정

application.properties 에 다음 설정을 추가하자. #MyBatis

 

주의! 웹 애플리케이션을 실행하는 main , 테스트를 실행하는 test 각 위치의 application.properties 를 모두 수정해주어야 한다.

 

main - application.properties

spring.profiles.active=local
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=

logging.level.org.springframework.jdbc=debug

#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=true

  • JdbcTemplate의 BeanPropertyRowMapper 에서 처럼 언더바를 카멜로 자동 변경해주는 기능을 활성화 한다. 관례의 불일치 참고

logging.level.hello.itemservice.repository.mybatis=trace

  • MyBatis에서 실행되는 쿼리 로그를 확인할 수 있다.
  • 'hello.itemservice.repository.mybatis'에서 실행되는 쿼리들이 대상

관례의 불일치

자바 객체에는 주로 카멜( camelCase ) 표기법을 사용한다. itemName 처럼 중간에 낙타 봉이 올라와 있는 표기법이다.

반면에 관계형 데이터베이스에서는 주로 언더스코어를 사용하는 snake_case 표기법을 사용한다. item_name 처럼 중간에 언더스코어를 사용하는 표기법이다.

이렇게 관례로 많이 사용하다 보니 map-underscore-to-camel-case 기능을 활성화 하면 언더스코어 표기법을 카멜로 자동 변환해준다.

따라서 DB에서 select item_name 으로 조회해도 객체의 itemName ( setItemName() ) 속성에 값이 정상 입력된다.

정리하면 해당 옵션을 켜면 snake_case 는 자동으로 해결되니 그냥 두면 되고, 컬럼 이름과 객체 이름이 완전히 다른 경우에는 조회 SQL에서 별칭을 사용하면 된다.

 

예)

DB select item_name

객체 name

 

별칭을 통한 해결방안

select item_name as name

 

참고: IBatis 는 MyBatis 의 예전 버전으로 MyBatis 라이브러리 패키지 명에 IBatis 로 남아 있는 것들이 있다. 

MyBatis 적용1 - 기본

MyBatis를 사용해서 데이터베이스에 데이터를 저장해보자.

XML에 작성한다는 점을 제외하고는 JDBC 반복을 줄여준다는 점에서 기존 JdbcTemplate과 거의 유사하다.

ItemMapper - MyBatis 매핑 파일

package hello.itemservice.repository.mybatis;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@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 매핑 파일을 만들어준다.

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

hello.itemservice.repository.mybatis -> src/main/resources/hello/itemservice/repository/mybatis

 

SQL 쿼리 XML 파일 (src/main/resources/hello/itemservice/repository/mybatis)

<?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 파일 경로 수정하기

기본은 Mapper 자바 파일과 XML 파일을 같은 경로, 같은 이름으로 두는 것이 원칙이다.

그러나 XML 파일을 원하는 위치에 두고 관리하고 싶다면 application.properties 에 다음과 같이 설정하면 된다.

mybatis.mapper-locations=classpath:mapper/**/*.xml

 

이렇게 하면 resources/mapper 를 포함한 그 하위의 모든 폴더에 있는 모든 XML을 XML 매핑 파일로 인식한다.

이 경우 파일 이름은 자유롭게 설정해도 된다.

참고로 테스트의 application.properties 파일도 함께 수정해야 테스트를 실행할 때 인식할 수 있다.

 

insert sql - save()

# mapper.java
void save(Item item);

# mapper.xml
<insert id="save" useGeneratedKeys="true" keyProperty="id">
     insert into item (item_name, price, quantity)
     values (#{itemName}, #{price}, #{quantity})
</insert>

 

  • Insert SQL -> <insert> 태그 사용
  • id 에는 매퍼 인터페이스에 설정한 메서드 이름 지정
  • 파라미터: #{} 문법 사용. 매퍼에서 넘긴 객체의 프로퍼티 이름 기입 ( getXxx -> #{xxx} )
  • #{} 문법을 사용하면 PreparedStatement 를 사용한다. JDBC의 ? 를 치환한다 생각하면 된다.
  • useGeneratedKeys 는 데이터베이스가 키를 생성해 주는 IDENTITY 전략일 때 사용한다.
  • keyProperty 는 생성되는 키의 속성 이름을 지정한다. Insert가 끝나면 파라미터로 받은 item 객체의 id 속성에 생성된 PK 값이 입력된다.

 

update sql - update()

# mapper.java
import org.apache.ibatis.annotations.Param;
void update(@Param("id") Long id, @Param("updateParam") ItemUpdateDto updateParam);

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

 

  • Update SQL -> <update> 태그 사용
  • update() 는 파라미터가 Long id , ItemUpdateDto updateParam 으로 2개이다.
  • 파라미터가 1개만 있으면 @Param 을 지정하지 않아도 되지만, 파라미터가 2개 이상이면 @Param 으로 이름을 지정해서 파라미터를 구분해야 한다.
  • #{@Param으로 지정한 이름.프로퍼티 이름} 으로 파라미터 값을 참조한다.

select sql - findById()

# mapper.java
Optional<Item> findById(Long id);

# mapper.xml
<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.type-aliases-package 속성을 지정한 덕분에 모든 패키지 명을 다 적지 않아도 된다. 그렇지 않으면 모든 패키지 명을 다 적어야 한다.
    • JdbcTemplate의 BeanPropertyRowMapper 처럼 SELECT SQL의 결과를 객체로 바로 변환해준다.
    • mybatis.configuration.map-underscore-to-camel-case=true 속성을 지정한 덕분에 언더스코어를 카멜 표기법으로 자동으로 처리해준다. ( item_name 로 조회 -> itemName 로 매핑)
  • 자바 코드에서 반환 객체가 하나이면 Item , Optional 과 같이 사용하면 되고, 반환 객체가 하나 이상이면 컬렉션을 사용하면 된다. 주로 List 를 사용한다.

select sql - findAll()

# mapper.java
List<Item> findAll(ItemSearchCond itemSearch);

# mapper.xml
<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> , <if> 같은 동적 쿼리 문법을 통해 편리한 동적 쿼리를 지원한다.

<if> 는 해당 조건이 만족하면 구문을 추가한다.

<where> 은 적절하게 where 문장을 만들어준다.

예제에서 <if>가 모두 실패하게 되면 SQL where 를 만들지 않는다.

예제에서 <if>가 하나라도 성공하면 처음 나타나는 and 를 where 로 변환해준다.

 

예) 

if test="itemName != null and itemName != ''" (성공)

if test="maxPrice != null" (성공)

=> SQL 동적 쿼리 생성

select id, item_name, price, quantity

from item

where and item_name like concat('%', #{itemName}, '%' (문법 오류)

    and price &lt;= #{maxPrice}

 

MyBatis 가 첫번째 and 를 제거하여 적절한 쿼리문을 완성한다.

 

XML 특수문자

가격을 비교하는 조건

and price &lt= #{maxPrice}

여기에 보면 <= 를 사용하지 않고 $lt;= 를 사용한 것을 확인할 수 있다.

XML에서 TAG가 시작하거나 종료할 때 < , > 와 같은 특수문자를 사용하기 때문에, 데이터 영역에 < , > 같은 특수 문자를 사용할 수 없기 때문이다.

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

 

XML CDATA 사용

다른 해결 방안으로는 XML에서 지원하는 CDATA 구문 문법을 사용하는 것이다.

이 구문 안에서는 특수문자를 사용할 수 있다.

대신 XML TAG도 단순 문자로 인식되기 때문에 , <where>, <if> 등이 적용되지 않는다.

 

<![CDATA[ 특수문자 포함 구문 ]]>

 

<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">
           <![CDATA[
              and price <= #{maxPrice}
           ]]>
        </if>
    </where>
</select>

 

특수문자와 CDATA 각각 상황에 따른 장단점이 있으므로 원하는 방법을 그때그때 선택하면 된다.