인프런/스프링 DB 2편

5)데이터 접근 기술 - MyBatis

backend dev 2023. 3. 8.

MyBatis 소개

 

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

 

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

 

JdbcTemplate과 비교해서 MyBatis의 가장 매력적인 점은

 

SQL을 XML에 편리하게 작성할 수 있고 또 동적 쿼리를 매우 편리하게 작성할 수 있다는 점이다.

 

먼저 SQL이 여러줄에 걸쳐 있을 때 둘을 비교해보자.

 

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  동적쿼리

@Override
public List<Item> findAll(ItemSearchCond cond) {
    String itemName = cond.getItemName();
    Integer maxPrice = cond.getMaxPrice();

    SqlParameterSource param = new BeanPropertySqlParameterSource(cond);

    String sql = "select id,item_name,price,quantity from item";

    //검색 조건에 따른 동적쿼리 부분
    if (StringUtils.hasText(itemName) || maxPrice != null) {
        sql += " where"; //이름조건이 있거나 가격조건이 있다면 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는 동적 쿼리를 매우 편리하게 작성할 수 있는 다양한 기능들을 제공해준다

 

 

설정의 장단점

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

 

반면에 MyBatis는 약간의 설정이 필요하다.

 

 

정리

프로젝트에서 동적 쿼리와 복잡한 쿼리가 많다면 MyBatis를 사용하고,

 

단순한 쿼리들이 많으면 JdbcTemplate을 선택해서 사용하면 된다.

 

물론 둘을 함께 사용해도 된다.

 

하지만 MyBatis를 선택했다면 그것으로 충분할 것이다.

 

 

참고 

강의에서는 MyBatis의 기능을 하나하나를 자세하게 다루지는 않는다.

 

MyBatis를 왜 사용하는지, 그리고 주로 사용하는 기능 위주로 다룰 것이다.

 

그래도 이 강의를 듣고 나면 MyBatis로 개발을 할 수 있게 되고

추가로 필요한 내용을 공식 사이트에서 찾아서 사용할 수 있게 될 것이다.

 

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

https://mybatis.org/mybatis-3/ko/index.html

 

MyBatis – 마이바티스 3 | 소개

마이바티스는 무엇인가? 마이바티스는 개발자가 지정한 SQL, 저장프로시저 그리고 몇가지 고급 매핑을 지원하는 퍼시스턴스 프레임워크이다. 마이바티스는 JDBC로 처리하는 상당부분의 코드와

mybatis.org


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
mybatis.type-aliases-package=hello.itemservice.domain
mybatis.configuration.map-underscore-to-camel-case=true
logging.level.hello.itemservice.repository.mybatis=trace

(테스트에도 동일하게 넣어줘야 테스트할때도 mybatis 설정이 적용된다.)

 

mybatis.type-aliases-package=hello.itemservice.domain

마이바티스에서 타입 정보를 사용할 때는 패키지 이름을 적어주어야 하는데,

(파라미터나,리턴타입(resultType)에 대한 정보를 적을때 해당 타입의 패키지이름까지 적어줘야하는데

미리 패키지를 설정해주면 생략가능하다.)

 

여기에 명시하면 패키지 이름을 생략할 수 있다. 지정한 패키지와 그 하위 패키지가 자동으로 인식된다.

 

여러 위치를 지정하려면 , , ; 로 구분하면 된다.

 

mybatis.configuration.map-underscore-to-camel-case=true

JdbcTemplate의 BeanPropertyRowMapper 에서 처럼 언더바를 카멜로 자동 변경해주는 기능을 활성화 한다.

 

(데이터베이스에는 컬럼명이 대부분 item_name처럼 스네이크케이스를 많이사용하고,

자바객체에는 itemName처럼 카멜케이스로 많이 사용된다.

sql의 결과로 가져온 결과인 item_name을 itemName으로 자동 변경해주는 기능을 활성화)

 

바로 다음에 설명하는 관례의 불일치 내용을 참고하자.

 

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

MyBatis에서 실행되는 쿼리 로그를 확인할 수 있다. ( mybatis 인터페이스들이 저장되는 곳으로 설정하면된다.)

 

 

관례의 불일치

자바 객체에는 주로 카멜( camelCase ) 표기법을 사용한다.

 

itemName 처럼 중간에 낙타 봉이 올라와 있는 표기법이다.

 

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

 

item_name 처럼 중간에 언더스코어를 사용하는 표기법이다.

 

이렇게 관례로 많이 사용하다 보니 map-underscore-to-camel-case 기능을 활성화 하면

언더스코어 표기법을 카멜로 자동 변환해준다.

 

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

 

정리하면 해당 옵션을 켜면 snake_case 는 자동으로 해결되니 그냥 두면 되고,

 

컬럼 이름과 객체 이름이 완전히 다른 경우에는 조회 SQL에서 별칭을 사용하면 된다

(별칭은  SQL의 as라는 기능을 이용하면 된다. ex) select item_name as name )

as == > alias(별칭,별명 이라는뜻)

 

참고

ibatis(아이바티스)라는 패키지이름이 보일수도있다 그것은 MyBatis의 예전버전이라고 한다.

 


MyBatis 적용 - 기본

 

이제부터 본격적으로 MyBatis를 사용해서 데이터베이스에 데이터를 저장해보자.

 

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

 

 

 ItemMapper

@Mapper // 인터페이스로 생성해주고 @Mapper 어노테이션을 붙인다.
public interface itemMapper {

    //여기에 메소드를 xml에서 불러서 진행할것이다.
    void save(Item item);

    //파라미터가 하나인경우에는 @Param을 넣어주지않아도 되는데 파라미터가 2개이상인경우에는 @Param을 넣어줘야한다.
    void update(@Param("id") Long id, @Param("updateParam") ItemUpdateDto updateParam);

    //파라미터로 넘어가는 ItemSearchCond의 필드(인스턴스 변수)를 사용할 수 있다.
    List<Item> findAll(ItemSearchCond itemSearchCond);

    Optional<Item> findById(Long id);
}

마이바티스 매핑 XML을 호출해주는 매퍼 인터페이스이다.

 

이 인터페이스에는 @Mapper 애노테이션을 붙여주어야 한다.

 

그래야 MyBatis에서 인식할 수 있다.

 

이 인터페이스의 메서드를 호출하면 다음에 보이는 xml 의 해당 SQL을 실행하고 결과를 돌려준다.

(

매퍼인터페이스에서 save라는 메소드를 호출하면 xml에서

<insert id="save" useGeneratedKeys="true" keyProperty="id"> 처럼

id가 save인 sql문을 실행시켜주고 결과를 반환한다.

)

 

ItemMapper 인터페이스의 구현체에 대한 부분은 뒤에 별도로 설명한다.

(구현체가 자동으로 만들어짐)

 

이제 같은 위치에 실행할 SQL이 있는 XML 매핑 파일을 만들어주면 된다.

 

참고로 자바 코드가 아니기 때문에 src/main/resources 하위에 만들되,

패키지 위치는 맞추어 주어야 한다. 이름도 맞춰준다.

 

(application.*에서 마이바티스 설정으로 xml파일 경로를 지정해서 원하는경로에 둘 수 있다.)

 

 

참고! (XML 파일 경로 수정)

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

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

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

(설정한 경로와 그 하위폴더에 있는 XML을 XML 매핑 파일로 인식)

 

이 경우 파일 이름은 자유롭게 설정해도 된다. (원래는 이름도 맞춰야했다)

 

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

 

xml 파일경로 설정 전

매퍼 인터페이스가 있는 경로를 맞춰 resources안에 위치시켜야하고, 이름또한 같아야했다.

XML 파일 경로 설정 후

설정한 경로 밑에다 두면된다. 이름도 자유롭게 하면된다.

 

 

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 xml 기본문법 -->

<mapper namespace="hello.itemservice.repository.mybatis.ItemMapper">
<!--패키지명 + 매퍼이름을 namespace 속성에 넣어줘야한다. 네임스페이스은 인터페이스 바인딩을 가능하게 한다-->

  <!--id는 매퍼인터페이스의 메소드명을 넣는다. useGeneratedKeys는 자동으로 기본키값을 생성해주는설정,keyProperty는 생성한 기본키값을 어느컬럼에 넣을지 설정-->
  <insert id="save" useGeneratedKeys="true" keyProperty="id">
    insert into item (item_name,price,quantity)
    values ( #{itemName},#{price},#{quantity} )
  <!--매퍼인터페이스를 보면 item객체가 넘어오는데 해당 item객체의 인스턴스변수값들이 #{}로 인해 들어가게 된다.
  마이바티스가 알아서 getItemName() 이런식으로 꺼내서 파라미터로 사용함-->

  </insert>

  <update id="update">
    update item
    <!--이 메소드는 파라미터가 2개이므로 @Param을 사용했다. 그리고 @Param의 속성으로 파라미터 이름을 부여했다. 그 이름을 이용하여 파라미터를 사용한다. -->
    set item_name = #{updateParam.itemName},
        price = #{updateParam.price},
        quantity = #{updateParam.quantity}
    where id = #{id}

  </update>
  <!--resultType은 sql의 반환타입을 설정해주는곳이다. 원래는 패키지명 + 클래스명까지 다들어가야하지만 application.properties에서
  마이바티스 설정중 mybatis.type-aliases-package 설정을 해줬기때문에 생략가능하다.-->
  <select id="findById" resultType="Item">
    select id, item_name, price, quantity
    from item
    where id = #{id}
    <!-- 파라미터가 하나라서 @Param을 생략한 파라미터는 , #{}에다 아무이름을 적어도 동작 되지만 의미있게 id로 적어준다.-->
  </select>

<!-- resultType에는 메소드의 반환타입이 아닌 sql 결과값을 어떤 클래스에 매핑할지를 적는곳이다.
 (collection인 경우 collection 타입 자체가 아닌 collection이 포함된 타입)
 메소드의 반환값은 List<Item>인 즉 collection타입은 알아서 만들어서 반환해주는것 같다 -->
  <select id="findAll" resultType = "Item>">
    select id,item_name,price,quantity
    from item
        <!-- <where>을 사용하면 <where>내부의 컨텐츠가 있다면(반환이있다면) where을 추가해준다.
        아무 조건이 붙지않는데 where가 붙는 경우를 방지하기 위한 태그-->
      <where>
        <!--<if>는 test라는 속성을 이용하여 test에 적은 조건이 참일경우 if태그 내부의 sql문을 추가해준다.
        아래 조건은 itemName이 null이 아니고, 공백이 아닐경우를 의미한다.
        첫번째 조건이라 and가 들어가면 안될것 같지만 if문 통과시 where가 알아서 and를 지워버린다.-->
        <if test="itemName != null and itemName != ''">
            and item_name like concat('%',#{itemName},'%')
        </if>
        <!-- xml파일이므로 <, >, &는 사용할수 없다 태그에 사용되는것이므로 그러므로 &lt와 같은 XML 특수문자를 이용해서 치환시킨다.-->
        <if test="maxPrice != null">
            and price &lt;= #{maxPrice}
        </if>
      </where>
  </select>


</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 xml 기본문법 -->

<mapper namespace="hello.itemservice.repository.mybatis.ItemMapper">
<!--패키지명 + 매퍼이름을 namespace 속성에 넣어줘야한다. 네임스페이스은 인터페이스 바인딩을 가능하게 한다-->

위쪽은 mapper xml 기본문법이고

 

<mapper> 태그는 namespace를 이용하여 어떤 매퍼 인터페이스를 바인딩하는 xml인지를 정해준다.

namespace는 매퍼 인터페이스의 패키지위치 + 클래스명을 적어주면 된다.

 

 

<insert>

<!--id는 매퍼인터페이스의 메소드명을 넣는다. useGeneratedKeys는 자동으로 기본키값을 생성해주는설정,keyProperty는 생성한 기본키값을 어느컬럼에 넣을지 설정-->
<insert id="save" useGeneratedKeys="true" keyProperty="id">
  insert into item (item_name,price,quantity)
  values ( #{itemName},#{price},#{quantity} )
<!--매퍼인터페이스를 보면 item객체가 넘어오는데 해당 item객체의 인스턴스변수값들이 #{}로 인해 들어가게 된다.
마이바티스가 알아서 getItemName() 이런식으로 꺼내서 파라미터로 사용함-->

</insert>

Insert SQL은 <insert>를 사용하면 된다.

 

id 에는 매퍼 인터페이스에 설정한 메서드 이름을 지정하면 된다.

여기서는 메서드 이름이 save() 이므로 save 로 지정하면 된다.

 

파라미터는 #{} 문법을 사용하면 된다. 그리고 매퍼에서 넘긴 객체의 프로퍼티 이름을 적어주면 된다

(객체의 프로퍼티 == 필드 , 인스턴스 변수)

 

#{} 문법을 사용하면 PreparedStatement 를 사용한다.     JDBC의 ? 를 치환한다 생각하면 된다.

(PreparedStatement를 이용해서 PreparedStatement.setString(순서,값) 이런식으로 ?에다가 파라미터를 매핑했던거처럼)

 

useGeneratedKeys 는 데이터베이스가 키를 생성해 주는 IDENTITY 전략일 때 사용한다.

(입력(insert, update)에만 적용) 데이터베이스에서 내부적으로 생성한 키 (예를들어 MySQL또는 SQL Server와 같은 RDBMS의 자동 증가 필드)를 받는 JDBC getGeneratedKeys메소드를 사용하도록 설정하다. 디폴트는 false 이다.)

 

keyProperty 는 생성되는 키의 속성 이름을 지정한다. Insert가 끝나면 item 객체의 id 속성에 생성된 값이 입력된다.

((입력(insert, update)에만 적용) getGeneratedKeys 메소드나 insert 구문의 selectKey 하위 엘리먼트에 의해 리턴된 키를 셋팅할 프로퍼티를 지정. 디폴트는 셋팅하지 않는 것이다. 여러개의 칼럼을 사용한다면 프로퍼티명에 콤마를 구분자로 나열할수 있다.)

 

생성된 데이터의 키값을 받기

데이터베이스 내부에서 자동으로 생성된 키값 ( auto increment등으로 인해 생성된)을

받아서 사용해야할때가 있을것이다.

 

useGeneratedKeys를 이용해서 키 자동생성을 설정하고

keyProperty 를 이용해서 <insert>에 전달되는 파라미터(객체) 내부의 컬럼에다가 해당 id값을 넣어준다.

 

그러므로useGeneratedKeys ,keyProperty 를 사용했다면 파라미터로 전달한 객체 내부의 id값이 들어있을것이다.

 

<update>

<update id="update">
  update item
  <!--이 메소드는 파라미터가 2개이므로 @Param을 사용했다. 그리고 @Param의 속성으로 파라미터 이름을 부여했다. 그 이름을 이용하여 파라미터를 사용한다. -->
  set item_name = #{updateParam.itemName},
      price = #{updateParam.price},
      quantity = #{updateParam.quantity}
  where id = #{id}

</update>

Update SQL은 <update> 를 사용하면 된다.

 

파라미터가 Long id , ItemUpdateDto updateParam 으로 2개이다.

 

파라미터가 1개만 있으면 @Param 을 지정하지 않아도 되지만,

파라미터가 2개 이상이면 @Param 으로 이름을 지정해서 파라미터를 구분해야 한다.

 

 

<select>    -   findById

<!--resultType은 sql의 반환타입을 설정해주는곳이다. 원래는 패키지명 + 클래스명까지 다들어가야하지만 application.properties에서
마이바티스 설정중 mybatis.type-aliases-package 설정을 해줬기때문에 생략가능하다.-->
<select id="findById" resultType="Item">
  select id, item_name, price, quantity
  from item
  where id = #{id}
  <!-- 파라미터가 하나라서 @Param을 생략한 파라미터는 , #{}에다 아무이름을 적어도 동작 되지만 의미있게 id로 적어준다.-->
</select>

Select SQL은 <select>를 사용하면 된다.

 

resultType 은 반환 타입을 명시하면 된다. 여기서는 결과를 Item 객체에 매핑한다.

 

(이 구문에 의해 리턴되는 기대타입의 패키지 경로를 포함한 전체 클래스명이나 별칭을 쓰면된다.

(application.properties에 한 마이바티스설정을 하면 별칭으로 적어도 된다.) 

collection인 경우 collection 타입 자체가 아닌 collection 이 포함된 타입을 적으면 된다.)

 

resultType은 매핑되는 메퍼 인터페이스 메소드의 반환타입

혹은 반환타입이 collection이라면 collection 내부의 타입을 적어주면 된다. 

 

JdbcTemplateBeanPropertyRowMapper 처럼 SELECT SQL의 결과를 편리하게 객체로 바로 변환해준다

(MyBatis는 resultType을 적어주는것만으로 알아서 변환처리해준다.)

+

(mybatis.configuration.map-underscore-to-camel-case=true 속성을 지정한 덕분에

언더스코어를 카멜 표기법으로 자동으로 처리해준다. ( item_name  --> itemName ))

 

자바 코드에서 반환 객체가 하나이면 Item , Optional 과 같이 사용하면 되고,

 

반환 객체가 하나 이상이면 컬렉션을 사용하면 된다. 주로 List 를 사용한다.

 

 

<select>   -   findAll

<!-- resultType에는 메소드의 반환타입이 아닌 sql 결과값을 어떤 클래스에 매핑할지를 적는곳이다.
 (collection인 경우 collection 타입 자체가 아닌 collection이 포함된 타입)
 메소드의 반환값은 List<Item>인 즉 collection타입은 알아서 만들어서 반환해주는것 같다 -->
  <select id="findAll" resultType = "Item>">
    select id,item_name,price,quantity
    from item
        <!-- <where>을 사용하면 <where>내부의 컨텐츠가 있다면(반환이있다면) where을 추가해준다.
        아무 조건이 붙지않는데 where가 붙는 경우를 방지하기 위한 태그-->
      <where>
        <!--<if>는 test라는 속성을 이용하여 test에 적은 조건이 참일경우 if태그 내부의 sql문을 추가해준다.
        아래 조건은 itemName이 null이 아니고, 공백이 아닐경우를 의미한다.
        첫번째 조건이라 and가 들어가면 안될것 같지만 if문 통과시 where가 알아서 and를 지워버린다.-->
        <if test="itemName != null and itemName != ''">
            and item_name like concat('%',#{itemName},'%')
        </if>
        <!-- xml파일이므로 <, >, &는 사용할수 없다 태그에 사용되는것이므로 그러므로 &lt와 같은 XML 특수문자를 이용해서 치환시킨다.-->
        <if test="maxPrice != null">
            and price &lt;= #{maxPrice}
        </if>
      </where>
  </select>

동적쿼리관련은 공식문서를 참고한다.

https://mybatis.org/mybatis-3/ko/dynamic-sql.html

 

 

XML 특수문자

<if test="maxPrice != null">
    and price &lt;= #{maxPrice}
</if>

여기에 보면 <= 를 사용하지 않고

&lt;=

 를 사용한 것을 확인할 수 있다.

 

그 이유는 XML에서는 데이터 영역에 < , > , & 같은 특수 문자를 사용할 수 없기 때문이다.

 

이유는 간단한데, XML에서 TAG가 시작하거나 종료할 때 < , > 와 같은 특수문자를 사용하기 때문이다.

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

//xml 특수문자들

 

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

 

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

(

구문 시작은 <![CDATA[  

구문 끝은 ]]>

)

 

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

 

(구문안에서는 태그를 안쓰고 간단한것만 쓴다면 CDATA 구문문법이 더 편리할수도 있을것 같다.)

 

( < , > , &를 많이써야하는경우에는 CDATA를 쓰고 아니라면 그냥 XML 특수문자를 쓰자.)

<if test="maxPrice != null">
     <![CDATA[
     and price <= #{maxPrice}
     ]]>
 </if>

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

 


MyBatis 설정과 실행

만든 Mapper 인터페이스, XML 매퍼를 사용해보자.

 

Repository

@Repository
@RequiredArgsConstructor
public class MyBatisItemRepository implements ItemRepository {

    private final ItemMapper itemMapper; // 매퍼 인터페이스를 주입받는다.
    //@Mapper가 붙어있으면 알아서 스프링이 구현체를 만들어서 빈에 등록하기 때문에 주입받을 수 있다.

    @Override
    public Item save(Item item) {
        itemMapper.save(item); // item의 id에는 생성된 id값이 들어가있음.
        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); // findByid는 반환이 있음.
    }

    @Override
    public List<Item> findAll(ItemSearchCond cond) {
        return itemMapper.findAll(cond);
    }
}

MyBatisItemRepository 는 단순히 ItemMapper 에 기능을 위임한다.

 

MyBatis.config

@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);
    }

}

 

테스트해보니 잘동작!


MyBatis - 분석

 

생각해보면 지금까지 진행한 내용중에 약간 이상한 부분이 있다.

ItemMapper 매퍼 인터페이스의 구현체가 없는데 어떻게 동작한 것일까?

 

@Mapper // 인터페이스로 생성해주고 @Mapper 어노테이션을 붙인다.
public interface ItemMapper {

    //여기에 메소드를 xml에서 불러서 진행할것이다.
    void save(Item item);

    //파라미터가 하나인경우에는 @Param을 넣어주지않아도 되는데 파라미터가 2개이상인경우에는 @Param을 넣어줘야한다.
    void update(@Param("id") Long id, @Param("updateParam") ItemUpdateDto updateParam);

    Optional<Item> findById(Long id);

    //파라미터로 넘어가는 ItemSearchCond의 필드(인스턴스 변수)를 사용할 수 있다.
    List<Item> findAll(ItemSearchCond itemSearchCond);
}

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

 

설정 원리

1. 애플리케이션 로딩 시점에 MyBatis 스프링 연동 모듈은 @Mapper 가 붙어있는 인터페이스를 조사한다.

 

2. 해당 인터페이스가 발견되면 동적 프록시 기술을 사용해서 ItemMapper 인터페이스의 구현체를 만든다.

 

3. 생성된 구현체를 스프링 빈으로 등록한다.

 

실제 동적 프록시 기술이 사용되었는지 간단히 확인해보자.

MyBatisItemRepository - 로그 추가

주입받은 ItemMapper를 로그를 통해 확인해보자.

log.info("itemMapper class = > {} ", itemMapper.getClass());

 

출력해보면 JDK 동적 프록시가 적용된 것을 확인할 수 있다

 

 

매퍼 구현체

마이바티스 스프링 연동 모듈이 만들어주는 ItemMapper 의 구현체 덕분에

 

인터페이스 만으로 편리하게 XML의 데이터를 찾아서 호출할 수 있다.

 

원래 마이바티스를 사용하려면 더 번잡한 코드를 거쳐야 하는데,

이런 부분을 인터페이스 하나로 매우 깔끔하고 편리하게 사용할 수 있다.

 

매퍼 구현체는 예외 변환까지 처리해준다.

 

MyBatis에서 발생한 예외를 스프링 예외 추상화인 DataAccessException 에 맞게 변환해서 반환해준다.

 

JdbcTemplate이 제공하는 예외 변환 기능을 여기서도 제공한다고 이해하면 된다.

 

 

정리

매퍼 구현체 덕분에 마이바티스를 스프링에 편리하게 통합해서 사용할 수 있다.

 

매퍼 구현체를 사용하면 스프링 예외 추상화도 함께 적용된다.

 

마이바티스 스프링 연동 모듈이 많은 부분을 자동으로 설정해주는데,

 

데이터베이스 커넥션, 트랜잭션과 관련된 기능도 마이바티스와 함께 연동하고, 동기화해준다

 

참고 

마이바티스 스프링 연동 모듈이 자동으로 등록해주는 부분은 MybatisAutoConfiguration 클래스를 참고하자.

 

 


MyBatis  - 동적 쿼리

MyBatis에서 자주 사용하는 주요 기능을 공식 메뉴얼이 제공하는 예제를 통해 간단히 정리해보자.

 

 

MyBatis 공식 메뉴얼 https://mybatis.org/mybatis-3/ko/index.html

 

MyBatis 스프링 공식 메뉴얼 http://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/

 

 

동적 SQL

마이바티스가 제공하는 최고의 기능이자 마이바티스를 사용하는 이유는 바로 동적 SQL 기능 때문이다.

 

동적 쿼리를 위해 제공되는 기능은 다음과 같다.

 

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

 

if

<select id="findActiveBlogWithTitleLike"
     resultType="Blog">
  SELECT * FROM BLOG
  WHERE state = ‘ACTIVE’
  <if test="title != null">
    AND title like #{title}
  </if>
</select>

해당 조건에 따라 값을 추가할지 말지 판단한다.

( if 태그 안에 test 속성에 조건을 적고 그 조건이 참이면 if태그 내부의 sql문이 추가 되는 식이다.)

 

내부의 문법은 OGNL을 사용한다. 자세한 내용은 OGNL을 검색해보자.

(OGNL문법은 간단하게 if의 test속성에 들어가는 것은 객체의 프로퍼티로 판단한다는것, 프로퍼티가 아닌 문자열임을 나타내려면 2가지 방법이 있다 . OGNL의 기본 개념과 2가지 방법을 알려면 아래 링크 참조)

https://jehuipark.github.io/java/mybatis_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>

자바에서는 switch 구문과 유사하며 마이바티스에서는 choose 엘리먼트를 제공한다.

<when>은 if 느낌, <otherwise> 는 else의 느낌이다.

 

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 를 지운다.

 

참고로 다음과 같이 trim 이라는 기능으로 사용해도 된다. 이렇게 정의하면 와 같은 기능을 수행한다.

<trim prefix="WHERE" prefixOverrides="AND |OR ">
  ...
</trim>

(override 속성은 오버라이드하는 텍스트의 목록을 제한한다.

결과는 override 속성에 명시된 것들을 지우고 with 속성에 명시된 것을 추가한다.)

 

set

update 문의 동적쿼리를 위함

<update id="updateAuthorIfNecessary">
  update Author
    <set>
      <if test="username != null">username=#{username},</if>
      <if test="password != null">password=#{password},</if>
      <if test="email != null">email=#{email},</if>
      <if test="bio != null">bio=#{bio}</if>
    </set>
  where id=#{id}
</update>

여기서 set 엘리먼트는 동적으로 SET 키워드를 붙히고 필요없는 콤마를 제거한다.

아마도 trim 엘리먼트로 처리한다면 아래와 같을 것이다.

<trim prefix="SET" suffixOverrides=",">
  ...
</trim>

foreach

동적 SQL 에서 공통적으로 필요한 것은 collection 에 대해 반복처리를 하는 것이다. 종종 IN 조건을 사용하게 된다.

예를들면

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

collection : 전달받은 인자  (인자 이름을 쓴다)

(Map이나 배열객체와 더불어 List, Set등과 같은 반복가능한 객체를 전달할 수 있다. 반복가능하거나 배열을 사용할때 index값은 현재 몇번째 반복인지를 나타내고 value항목은 반복과정에서 가져오는 요소를 나타낸다. Map을 사용할때 index는 key객체가 되고 항목은 value객체가 된다.)

 

item : 전달받은 인자 값을 alias 명으로 대체 (반복문으로 돌아가며 받아오는 값에 대한 별칭을 적는다)

open : 구문이 시작될때 삽입할 문자열

close : 구문이 종료될때 삽입할 문자열

separator : 반복 되는 사이에 출력할 문자열

index : 반복되는 구문 번호이다. 0부터 순차적으로 증가

 

예시)

<foreach collection="chkList" item="item" open="(" close=")" separator=",">
 #{item.authority}
</foreach>

 

컬렉션을 반복 처리할 때 사용한다.

where in (1,2,3,4,5,6) 와 같은 문장을 쉽게 완성할 수 있다. 파라미터로 List 를 전달하면 된다.

 

 

MyBatis - 기타 기능

 

애노테이션으로 SQL 작성

다음과 같이 XML 대신에 애노테이션에 SQL을 작성할 수 있다.

 

Mapper 인터페이스의 메소드 위에다가 어노테이션을 이용하면 된다.

@Mapper // 인터페이스로 생성해주고 @Mapper 어노테이션을 붙인다.
public interface ItemMapper {

    //여기에 메소드를 xml에서 불러서 진행할것이다.
    void save(Item item);

    //파라미터가 하나인경우에는 @Param을 넣어주지않아도 되는데 파라미터가 2개이상인경우에는 @Param을 넣어줘야한다.
    void update(@Param("id") Long id, @Param("updateParam") ItemUpdateDto updateParam);

    @Select("select id, item_name, price, quantity from item where id=#{id}")
    Optional<Item> findById(Long id);

하지만 sql문을 자바로 적지않으려고 MyBatis를 사용하는것이므로 자주사용되는건 아니라고 한다.

 

@Insert , @Update , @Delete , @Select 기능이 제공된다.

 

이 경우 XML에는 

<select id="findById" resultType="hello.itemservice.domain.Item">
 ~~~~~~
</select>

는 제거해야 한다. 동적 SQL이 해결되지 않으므로 간단한 경우에만 사용한다.

 

어노테이션 관련 내용은 다음 링크에서 확인한다.

https://mybatis.org/mybatis-3/ko/java-api.html

 

 

문자열 대체(String Substitution)

#{} 문법은 ?를 넣고 파라미터를 바인딩하는 PreparedStatement 를 사용한다.

때로는 파라미터 바인딩이 아니라 문자 그대로를 처리하고 싶은 경우도 있다. 이때는 ${} 를 사용하면 된다.
@Select("select * from user where ${column} = #{value}")

User findByColumn(@Param("column") String column, @Param("value") Stringvalue);

${column}에는 ? 를 넣고 파라미터 바인딩을 할 수 없는 자리이다. 이자리에 직접 문자를 넣고 싶다면 ${}를 사용하면 된다.

 

주의

${} 를 사용하면 SQL 인젝션 공격을 당할 수 있다.

따라서 가급적 사용하면 안된다. 사용하더라도 매우 주의깊게 사용해야 한다.

 

 

재사용 가능한 SQL 조각

<sql>을 사용하면 SQL 코드를 재사용 할 수 있다.

(<sql> 엘리먼트는 다른 구문에서 재사용가능한 SQL구문을 정의할 때 사용된다.

로딩시점에 정적으로 파라미터처럼 사용할 수 있다. 다른 프로퍼티값은 포함된 인스턴스에서 달라질 수 있다.)

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

이렇게 <sql>를 이용해서 자주 사용하는 sql 코드조각을 만들어두고

 

SQL 조각은 다른 구문에 포함시킬수 있다. <include>를 이용하여

<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> 내부의 property 이름,값은   <include> 내부의 refid 속성이나 <include>태그 내부에서 사용될 수 있다. 

다음 예제를보면

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

 

 

sominclude라는 코드조각을 부르면서 property 속성을 이용해서

이름 : include_target에 sometable을 주고

그 include_target은 someinclude라는 sql 조각안에서 다른 sql 조각을 부를때 사용하고

 

이름 : prefix 값 : Some은

불러진 다른 sql 조각인 sometable 내부에서 사용된다.

 

https://mybatis.org/mybatis-3/ko/sqlmap-xml.html

(프로퍼티 값을 전달할 수 있고, 해당 값은 내부에서 사용할 수 있다.)

 

 

Result Maps

만약 컬럼명은 user_id인데 그 값을 저장할 객체의 프로퍼티명은 id이라면? ( 즉 컬럼명과 프로퍼티명이 다르다면)

(스네이크케이스를 카멜케이스로 바꿔주는 설정은 있지만 아예 이름이 다르다면)

별칭(as == alias)을 이용해야한다.

별칭을 이용하면 resultType 객체에 매핑할때 참고해서 매핑해준다. 

 

별칭을 이용한 예시)

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

 

칼럼명과 프로퍼티명이 다른 경우에 대해 데이터베이스 별칭을 사용하는 것과 다른 방법으로

명시적인 resultMap 을 선언하는 방법이 있다.

 

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


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

resultMap을 미리 만들어서 sql쿼리의 결과를 어떻게 객체에 매핑할것인지를 정의해둔다

(JdbcTemplate에서 RowMapper를 만들어놓는거랑 비슷한 느낌)

 

그리고 select같은 조회 sql에서 resultType이 아닌, resultMap 속성을 이용해주면 된다.

 

 

복잡한 결과매핑

 

만약 이런저런 테이블과 join으로 엮어있는 복잡한 쿼리라면 어떻게 결과매핑을 해야할까?

<!-- 매우 복잡한 구문 -->
<select id="selectBlogDetails" resultMap="detailedBlogResultMap">
  select
       B.id as blog_id,
       B.title as blog_title,
       B.author_id as blog_author_id,
       A.id as author_id,
       A.username as author_username,
       A.password as author_password,
       A.email as author_email,
       A.bio as author_bio,
       A.favourite_section as author_favourite_section,
       P.id as post_id,
       P.blog_id as post_blog_id,
       P.author_id as post_author_id,
       P.created_on as post_created_on,
       P.section as post_section,
       P.subject as post_subject,
       P.draft as draft,
       P.body as post_body,
       C.id as comment_id,
       C.post_id as comment_post_id,
       C.name as comment_name,
       C.comment as comment_text,
       T.id as tag_id,
       T.name as tag_name
  from Blog B
       left outer join Author A on B.author_id = A.id
       left outer join Post P on B.id = P.blog_id
       left outer join Comment C on P.id = C.post_id
       left outer join Post_Tag PT on PT.post_id = P.id
       left outer join Tag T on PT.tag_id = T.id
  where B.id = #{id}
</select>

별칭을 이용하는 방법도 있지만

매번 select를 사용할때마다 각 컬럼명에다가 as를 붙여주기 불편할것이다.

(반복작업이 될수있다.)

그러므로 select를 자주사용할거 같다면

resultMap을 이용하여 미리 매핑에 대한 정보를 등록해놓는것이다.

 

resultMap을 사용하기로 했다면 이때는

<association> , <collection>

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

 

JPA는 객체와 관계형 데이터베이스를 ORM 개념으로 매핑하기 때문에 이런 부분이 자연스럽지만,

 

MyBatis에서는 들어가는 공수도 많고, 성능을 최적화하기도 어렵다.

 

따라서 해당기능을 사용할 때는 신중하게 사용해야 한다.

 

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

https://mybatis.org/mybatis-3/ko/sqlmap-xml.html

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

댓글