제육's 휘발성 코딩
article thumbnail
반응형

MyBatis

Mybatis는 JdbcTemplate보다 더 많은 기능을 제공하는 SQL Mapper이다. 기본적으로 JdbcTemplate이 제공하는 기능을 대부분 제공하며, XML과 동적 쿼리를 매우 편리하게 작성할 수 있다는 장점이 있다.

 

MyBatis vs JdbcTemplate

JdbcTemplate과 MyBatis의 코드 차이를 알아보자.

JdbcTemplate

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>
  • XML을 기반으로 라인이 길어져도 문자 더하기에 대한 불편함이 없다.

 

JdbcTemplate - 동적 쿼리

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

build.gradle

//MyBatis 추가
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'
  • build.gradle에 mybatis-spring-boot-starter 라이브러리를 주입하면 스프링과 통합되고, 설정도 간단히 할 수 있다. 참고로 스프링 부트에서 제공해주는 라이브러리는 버전 관리를 자동적으로 해주지만 해당 라이브러리의 경우 스프링의 라이브러리가 아니기 때문에 버전을 명시해서 주입해야 한다.

 

main, test - 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
  • type-aliases-package
    • 마이바티스에서 타입 정보를 사용할 때는 패키지 이름을 적어주어야 하는데, 여기에 명시하면 패키지 이름을 생략할 수 있다.
    • 지정한 패키지와 그 하위 패키지가 자동으로 인식된다.
    • 여러 위치를 지정하려면 ,, ; 로 구분하면 된다.
  • configuration.map-underscore-to-camel-case
    • JdbcTemplate의 BeanPropertyRowMapper 처럼 스네이크를 카멜케이스로 자동 변경을 해주는 기능을 활성화 한다.
    • 참고로 DB 컬럼과 애플리케이션의 속성이 카멜,스네이크 차이가 아닌 아예 다른 경우에는 애플리케이션 레벨에서 직접 alias를 쿼리문에 적용시켜 해결한다.

 

MyBatis 적용 1 - 기본

ItemMapper

package hello.itemservice.repository.mybatis;

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 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);
}
  • MyBatis 매핑 XML을 호출해주는 매퍼 인터페이스를 생성하자. 이 인터페이스에는 @Mapper를 설정해줘야 MyBatis에서 인식할 수 있다.
  • 해당 인터페이스의 메서드를 호출하면 메스드명을 통해 xml의 해당 SQL을 실행하고 결과를 반환한다.
  • @Param은 파라미터가 1개일 때는 생략해도 되지만, 2개 이상이면 반드시 지정해서 파라미터를 구분해줘야 한다.

 

ItemMapper.xml

이제 실행 SQL이 있는 XML 매핑 파일을 만들어주면 된다. 여기서 주의할 점은 자바를 제외한 다른 파일들은 resources에 보관하는게 원칙이다. 다음과 같이 디렉토리를 동일하게 맞추어서 xml 파일을 생성해야 한다.

image

  • 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">
        select id, item_name, price, quantity
        from item
        where id = #{id}
    </select>

    <select id="findAll">
        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 : 앞서 만든 매퍼 인터페이스의 경로를 지정한다.
  • 파라미터는 #{}로 지정한다. 이 문법은 PrepareStatement를 사용해서 바인딩 해준다.
  • useGeneratedKeys는 데이터베이스 키 생성이 IDENTITY 전략일 때 사용한다.
  • &lt;< 를 의미하며 XML에서 특수 문자를 표시하기 위해 사용한다. (XML은 태그로 인식하기 때문)
    • > : &gt; , & : &amp; 등이 있다.
  • <where>, <if> 와 같은 동적 쿼리 문법을 통해 동적 쿼리를 사용할 수 있다. if가 모두 실패하면 where를 만들지 않고 하나라도 성공하면 처음 나타나는 andwhere 로 변환해서 적용시킨다.

 

XML CDATA 적용

<if test="maxPrice != null">
  <![CDATA[
  and price <= #{maxPrice}
  ]]>
</if>
  • 다음과 같이 CDATA를 사용하면 특수 문자를 XML 내부에서 사용할 수 있다.

참고 - XML 파일 경로 수정하기

XML 파일을 원하는 위치에 두고 싶으면 application.properties에 설정을 통해 적용할 수 있다.

mybatis.mapper-locations=classpath:mapper/**/*.xml 과 같이 resources/mapper 를 포함한 그 하위 폴더에 있는 XML을 매핑 파일로 인식한다.

 

MyBatis 적용 2 - 설정과 실행

MyBatisItemRepository

package hello.itemservice.repository.mybatis;

import hello.itemservice.domain.Item;
import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.ItemSearchCond;
import hello.itemservice.repository.ItemUpdateDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

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

@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 인터페이스를 단순히 위임하는 구현체 레포지토리를 생성하자.

MyBatisConfig

package hello.itemservice.config;

import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.mybatis.ItemMapper;
import hello.itemservice.repository.mybatis.MyBatisItemRepository;
import hello.itemservice.service.ItemService;
import hello.itemservice.service.ItemServiceV1;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

}
  • ItemMapper를 주입 받아, 필요한 의존관계를 빈 등록하자.

ItemServiceApplication

@Import(MyBatisConfig.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {
  • 컨트롤러만 스캔하므로 별도의 설정파일을 등록하자.

MyBatis 설정 원리

image

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

 

MyBatis 기능 정리 1 - 동적 쿼리

마이바티스를 사용하는 이유는 바로 동적 SQL 기능 때문이다. if, choose (when, otherwise), trim(where, set), foreach 등 여러 기능을 제공한다.

자세한 내용은 공식 문서를 참고하자. https://mybatis.org/mybatis-3/ko/dynamic-sql.html

 

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>
  • switch 구문과 비슷하게 구문을 사용할 수 있다.

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>
  • 이 예제는 문장을 모두 만족하지 않을 때 쿼리문의 문제가 발생한다. 즉, 사용하지 않는다.

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 태그를 사용하면 만족하는 하위 조건이 없으면 추가하지 않고, 있으면 첫 번째 AND가 WHERE로 변환 된다.

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>
  • 컬렉션을 반복 처리할 때 사용한다.파라미터로 List를 전달하여 사용한다.

 

MyBatis 기능 정리 2 - 기타 기능

애노테이션으로 SQL 작성

@Select("select id, item_name, price, quantity from item where id=#{id}")
Optional<Item> findById(Long id);
  • @Insert, @Update, @Delete, @Select 등의 기능을 제공한다.
  • 이 경우 XML에는 crud 태그를 제거해야 한다. 동적 SQL이 해결되지 않으므로 간단한 경우에 사용한다.

문자열 대체

#{} 문법은 ?를 넣고 파라미터를 바인딩하는 PrepareStatement를 사용한다. 때로는 파라미터 바인딩이 아니라 문자 그대로 처리하고 싶은 경우도 있다. 이 때는 ${} 를 사용하면 된다.

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

재사용 가능한 SQL 조각

sql 태그를 사용하면 SQL 코드를 재사용 할 수 있다.

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

<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 조각을 찾아 사용할 수 있다.

Result Maps

<resultMap id="userResultMap" type="User">
    <id property="id" column="user_id" />
    <result property="username" column="username"/>
    <result property="password" column="password"/>
  </resultMap>
  <select id="selectUsers" resultMap="userResultMap">
    select user_id, user_name, hashed_password
    from some_table
    where id = #{id}
</select>
  • 별칭(as)를 사용하고 싶을 때 resultMap을 선언해서 사용할 수 있다.

복잡한 결과매핑

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

이 때 association, collection 등의 태그를 사용하지만, 성능과 실효성 측면에서 불리한 면이 많기 때문에 고민이 필요하다.


REFERENCES

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2/

반응형
profile

제육's 휘발성 코딩

@sasca37

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요! 맞구독은 언제나 환영입니다^^