GitHub

https://github.com/Choidongjun0830

Spring

[Spring DB2] 데이터 접근 기술 - MyBatis

gogi masidda 2024. 3. 18. 21:20

MyBatis는 JdbcTemplate보다 더 많은 기능을 제공하는 SQL Mapper이고, JdbcTemplate가 제공하는 대부분의 기능을 제공한다. 하지만 MyBatis가 매력적인 점은 SQL을 XML에 편리하게 작성할 수 있고, 동적 쿼리를 매우 편리하게 작성할 수 있다는 점이다. 

 

MyBatis를 사용하기 위해서

//build.gradle에 추가

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

//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 = hello.itemservice.domain 
    • MyBatis에서 타입 정보를 사용할 때 패키지 명도 적어주어야 하는데, 이렇게 명시하면 패키지 이름을 생략할 수 있다.
    • 지정한 패키지와 그 하위 패키지가 자동으로 인식된다.
  • mybatis.configuration.map-underscore-to-camel-case = true
    • 언더바를 카멜로 자동 변경해주는 역할을 한다.
    • 관계형 DB에서 사용하는 item_name을 자바 객체에서 사용하는 카멜 케이스인 itemName으로 바꾸어준다.
    • 이 옵션을 켜면, 컬럼 이름과 객체 이름이 아예 다른 경우에만 SQL의 as 기능을 사용하면 된다.
  • logging.level.hello.itemservice.repository.mybatis = trace
    • MyBatis에서 실행되는 쿼리 로그를 확인할 수 있다.

MyBatis 적용1 - 기본

xml에 저장한다는 점을 빼면 JDBC의 반복을 줄여준다는 점에서 JDBCTemplate과 유사하다.

 

@Mapper
public interface ItemMapper {

    void save(Item item);

    //파라미터가 두개가 넘어가는 경우는 @Param을 넣어줘야함
    void update(@Param("id") Long id, @Param("updateParam") ItemUpdateDto updateDto);

    List<Item> findAll(ItemSearchCond itemSearchCond);

    Optional<Item> findById(Long id);
}

이렇게 MyBatis 매핑 파일을 만들어주어야 한다.

  • MyBatis 매핑 XML을 호출해주는 매퍼 인터페이스이다.
  • @Mapper를 붙여주어야 MyBatis에서 인식할 수 있다.
  • 이 인터페이스의 메서드를 호출하면, xml의 해당 SQL을 실행하고 결과를 돌려준다.

같은 위치에 실행할 SQL이 있는 XML 매핑 파일을 만들어주면 된다. 자바 파일이 아니라서 src/main/resources의 하위에 만들되, 패키지 위치는 맞춰주어야 한다.

 

ItemMapper.xml

// 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는 앞에서 만든 Mapper의 위치를 지정한다.
  • save()
    • <insert> 사용
    • id에는 매퍼 인터페이스에 설정한 메서드 이름인 save를 넣는다.
    • 파라미터는 #{} 문법을 사용한다. JdbcTemplate의 ?를 치환한다 생각하면 된다.
    • useGeneratedKeys는 데이터베이스가 키를 생성해주는 것을 사용할 때 쓰면되고, keyProperty는 생성되는 키의 속성 이름을 지정한다. 그러면 insert가 끝나면 item 객체의 id속성에 생성된 값이 입력된다.
  • update()
    • <update> 사용
    • 인터페이스에서는 파라미터가 2개 이상이면 @Param으로 지정해서 파라미터를 구분한다.
    • xml에서는 updateParam의 itemName, price, quantity를 updateParam.itemName 처럼 사용할 수 있다.
  • findById()
    • <select> 사용
    • resultType은 반환 타임을 명시한다.
      • 앞에서 application.properties에 'mybatis.type-aliasespackage=hello.itemservice.domain'을 지정해두어서 모든 패키지명을 다 쓰지 않아도 Item만 적어서 사용할 수 있다.
      • select의 결과를 객체로 바로 변환해준다.
      • 'mybatis.configuration.map-underscore-to-camel-case=true'을 지정해두어서 item_name을 itemName으로 자동으로 처리해준다.
    • 자바 코드에서 변환 객체가 하나면 Item, Optional<Item>처럼 사용하면되고, 하나 이상이면 컬렉션을 사용하면된다. 주로 List를 쓴다.
  • findAll()
    • <select> 사용
    • <where>, <if> 동적 쿼리 문법을 사용한다.
      • <if>문이 모두 실패하면 SQL은 where절을 만들지 않는다.
      • <if>문이 하나라도 성공하면 and를 where로 변환해준다.
  • XML 특수 문자
    • < : &lt;
    • > : &rt;
    • & : &amp;

MyBatis 적용2 - 설정과 실행

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

ItemRepository는 ItemMapper에게 기능을 위임하는 식으로 구현하면 된다.

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

 

이렇게 하면 원래 테스트도 잘 작동하고 기존 코드도 localhost에서 잘 돌아가는 것을 확인할 수 있다.

 

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

MyBatis 기능 정리

동적 SQL

제공되는 기

  • if
    • 해당 조건에 따라 값을 추가할지 말지
    • 내부의 문법은 OGNL 사용
  • choose (when, otherwise)
    • 자바의 switch 구문과 유사
  • trim (where, set)
    • and가 먼저 시작되면 and를 where로 바꿈
  • foreach
    • 컬렉션을 반복처리할 때 사용
    • 파라미터로 List를 전달하면 된다.
//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>
// 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>

 

기타 기능

  • 애노테이션으로 SQL 작성
    • @Insert, @Update, @Delete, @Select 기능이 제공
    • 아래의 경우에는 XML에 있는 findById에 대한 것은 제거해야한다.
    • 동적 SQL이 해결되지 않아서 간단한 경우에만 사용한다.
    • 하지만 잘 사용하지 않는다.
@Select("select id, item_name, price, quantity from item where id=#{id}")
Optional<Item> findById(Long id);
  • 문자열 대체
    •  #{} 기능은 ?를 넣고 파라미터를 바인딩하는 PreparedStatement 방식을 사용한다.
    • 파라미터 바인딩이 아니라 문자 그대로를 처리하고 싶은 경우에는 ${}를 사용하면 된다.
    • SQL 인젝션 공격을 당할 수 있어서 가급적 사용하면 안된다.
@Select("select * from user where ${column} = #{value}")
User findByColumn(@Param("column") String column, @Param("value") String value);
  • 재사용 가능한 SQL 조각
    • <sql>을 사용하면 SQL코드를 재사용할 수 있다.
    • 별도로 SQL 문을 정의해서 <include>를 통해서 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>
...
  • ResultMap
    • 컬럼명과 객체 프로퍼티 명이 다를 때 as를 사용할 수 있다.
    • 하지만 as를 사용하지 않고도 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>

 

728x90