Spring DB : MyBatis

    이 글은 인프런 김영한님의 강의를 복습하며 작성한 글입니다. 


    MyBatis의 장점

    • SQL을 XML에 편리하게 작성할 수 있다. (라인이 길어져도 문자 더하기에 대한 불편함이 없다. 그림 추가)
    • 동적 쿼리를 매우 편리하게 작성할 수 있다. 

    MyBatis의 단점

    • MyBatis는 약간의 설정이 필요하다. 

    프로젝트에서 동적 쿼리와 복잡한 쿼리가 많다면 MyBatis를 사용하는 것이 좋다. 단순한 쿼리가 많은 경우 JdbcTemplate을 선택해서 사용하는 것이 좋다. 둘을 함께 사용해도 좋다. 

     

    MyBatis의 설정

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

    MyBatis를 사용하기 위해서는 MyBatis를 설정해야한다. 설정을 위해서 위의 코드를 build.gradle에 추가해서 의존성 주입을 한다.

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

    다음 설정값도 application.properties에 추가해둔다. 그리고 테스트 코드에서도 사용할 수 있기 때문에 테스트 영역의 application.properties에도 해당 설정값을 추가해준다. 

    • mybatis.type-aliases-package=hello.itemservice.domain
      • MyBatis에서는 returnType에 클래스명을 알려줄 때, 이 때 패키지명까지 다 작성해야한다. 이 패키지명을 작성하는 부분을 해결하기 위해서 위 패키지를 사용한다. 
      • 지정한 패키지와 그 하위 패키지가 자동으로 인식된다.
      • 여러 위치를 지정하려면 ',' / ';'로 구분하면 된다.
    • mybatis.configuration.map-underscore-to-camel-case
      • JdbcTemplate의 BeanPropertyRowMapper에서 처럼 언더바를 카멜로 자동 변경해주는 기능을 활성화 한다.

     

    관례의 불일치

    자바 객체에서는 주로 카멜 표기법을 사용하고, RDBMS에서는 언더 스코어를 사용한다. 위 설정을 활성화하면 이 관례의 불일치를 자동으로 맵핑해서 넣어준다. 

     

     

    ItemMapper 생성하기 (인터페이스)

    @Mapper
    public interface ItemMapper {
        // 반드시 인터페이스로 만들어야 함
    
        // 파라미터가 1개인 경우는 넣지 않아도 됨.
        void save(Item item);
    
        // 파라미터가 2개인 경우, @Param을 넣어줘야함.
        void update(@Param("id") Long id, @Param("updateParam") ItemUpdateDto dto);
    
        // 파라미터가 1개인 경우는 넣지 않아도 됨.
        List<Item> findAll(ItemSearchCond itemSearch);
    
        // 파라미터가 1개인 경우는 넣지 않아도 됨.
        Optional<Item> findById(Long id);
    }
    • MyBatis 맵핑 XML을 호출해주는 매퍼 인터페이스다.
    • 인터페이스에는 @Mapper를 붙어주어야 함. 그래야 MaBatis에서 인식해줌.
    • 인터페이스의 메서드를 호출하면, 'XML'의 해당 SQL을 실행하고 결과를 돌려줌.
    • SQL을 실행할 'XML'을 구현해야함.

    Mapper와 같은 위치에 실행할 SQL이 있는 XML 맵핑 파일을 만들어 줘야한다. 자바 코드가 아니기 때문에 src/main/resources 하위에 만들되, 패키지 위치를 동일하게 맞춰줘야한다.

    위처럼 패키지 상에서 동일한 위치에 ItemMapper.xml을 동일한 이름으로 만들어 줘야한다. 

     

     

    IteMapper XML 만들기 (인터페이스용)

    Insert-Save 쿼리

    <!--    id는 메서드 명을 입력해줘야 함. -->
    <!--    Values에는 Item 객체에 있는 필드 명을 입력해주면 됨. -->
    <!--    키를 생성하고, 키 값은 "id"라고 저장해 줌.-->
        <insert id="save" useGeneratedKeys="true" keyProperty="id">
            insert into item (item_name, price, quantity)
            values (#{itemName}, #{price}, #{quantity})
        </insert>
    • Insert SQL은 <insert>를 이용하면 됨
    • 'id'에는 Mapper 인터페이스에 설정한 메서드 이름을 지정하면 됨. 여기서는 메서드 이름이 save() 이므로 save로 지정함. 
    • 파라미터는 #{}로 사용하면 됨.
    • #{} 문법을 사용하면 PreparedStatement를 사용함. JDBC의 '?'를 치환하는 것으로 이해하면 됨. 
    • userGeneratedKeys는 DB 키를 생성해주는 Identity 전략일 때 사용함. KeyProperty는 생성되는 키의 속성 이름을 지정한다. Insert가 끝나면 Item 객체의 id 속성에 생성된 값이 저장됨.  

     

    Updatet-update 쿼리

    <update id="update">
        update item
        set item_name=#{updateParam.itemName},
            price=#{updateParam.price},
            quantity=#{updateParam.quantity}
        where id=#{id}
    </update>
    • Update SQL은 <Update>를 사용하면 됨.
    • 파라미터가 Long id, ItemUpdateDto updateParam으로 2개가 존재한다. 파라미터가 2개 이상일 때는 @Param을 지정해서 파라미터를 구분해야 함. 

     

    Select - findById

    <select id="findById" resultType="item">
        select id, item_name, price, quantity
        from item
        where id=#{id}
    </select>
    • Select SQL은 <select>를 사용하면 됨. 
    • resultType을 명시해야 한다. 여기서 결과를 "Item" 객체에 맵핑한다.
      • application.properties에서 aliases-pacakage 설정값 때문에 모든 패키지 명을 적지 않아도 됨. 
      • JdbcTemplate의 BeanPropertyMapper처럼 SELECT SQL의 결과를 편리하게 객체로 바로 변환해줌. 
      • application.properties의 설정 때문에 언더스코어를 카멜 표기법으로 자동 처리해줌.
    • 자바 코드에서 반환 객체가 하나이면 Item, Optional<Item>으로 사용하면 됨. 하나 이상이면 컬렉션을 사용하면 됨. 주로 List를 사용함. 

     

    select - findAll

    <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"로 변환해줌. 

     

    XML 특수문자 주의 사항

    and price &lt;= #{maxPrice}

    위 쿼리에서 가격 비교를 위해 <=를 '&lt;='로 표현했다. 이유는 XML에서는 데이터 영역에 <,> 문자를 사용할 수 없기 때문이다. 따라서 XML에서 대소 비교는 아래 문자를 이용해야 한다. 

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

     

    MyBatisItemRepository 만들어서 테스트

    설정이 완료되었으면, MyBatisItemRepository를 만든다. 

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

    만든 후에, 설정 클래스에서 MyBatis를 사용하도록 변경한다. 

    @Import(MyBatisConfig.class)
    @SpringBootApplication(scanBasePackages = "hello.itemservice.web")
    @Slf4j
    public class ItemServiceApplication {
    ...
    }

    Repository를 생성하고, 설정 클래스를 변경한 다음 ItemRepositoryTest를 해보면 정상적으로 테스트가 수행되는 것을 확인할 수 있다.

     

    MyBatis의 구현체

    앞서 개발자는 ItemMapper라는 인터페이스와 XML 파일만 만들었다. 실제 인터페이스의 구현체를 만들지 않았는데, 어떻게 의존성 주입이 되고 동작할 수 있었던 것일까? 

    이 부분은 MyBatis 스프링 연동 모듈에서 자동으로 위와 같이 처리해 줌. 

    1. 어플리케이션 로딩 시점에 MyBatis 스프링 연동 모듈은 @Mapper가 붙은 인터페이스를 조사함.
    2. 인터페이스가 확인되면 동적 프록시 기술을 사용해서 "ItemMapper" 인터페이스의 구현체를 만듦.
    3. 생성된 구현체를 스프링 빈으로 등록함. 

    즉, AOP와 같이 동적 프록시 객체를 만들어서 스프링빈으로 등록하고 의존성을 주입해준다는 것이다. 

     

    실제 동적 프록시 기술이 적용되었는지 확인

    @Override
    public Item save(Item item) {
        log.info("itemMapper Class = {}", itemMapper.getClass());
        itemMapper.save(item);
        return item;
    }

    다음 클래스에 Log를 찍어서, ItemMapper의 클래스를 확인해본다.

    ItemMapper 클래스를 확인해보니 "com.sun.proxy"다. 즉, JDK 동적 프록시를 이용해서 만들어진 프록시가 주입되었다는 것을 확인할 수 있었다.

     

    매퍼 구현체

    • MyBatis 스프링 연동 모듈이 만들어주는 ItemMapper의 구현체 덕분에 인터페이스 만으로 편리하게 XML의 데이터를 찾아서 호출할 수 있음.
    • 매퍼 구현체는 예외 반환까지 처리해준다. MyBatis에서 발생한 예외를 스프링 예외 추상화인 DataAccessException에 맞게 변환해서 반환해준다. JdbcTemplate이 제공하는 예외 변환 기능을 여기서도 제공한다고 이해하면 된다. 

     

    정리

    • 매퍼 구현체 덕분에 MyBatis를 스프링에 편리하게 통합해서 사용할 수 있음.
    • 매퍼 구현체를 사용하면 스프링 예외 추상화도 함께 적용됨. 
    • MyBatis 스프링 연동 모듈(MyBatisAutoConfigutarion 클래스 참고) 이 많은 부분을 자동화 해줌. DB 커넥션, 트랜잭션과 관련된 기능도 마이바티스와 함께 연동하고 동기화 해줌. 
    • MyBatis 모듈은 DataSource / TransacationManager를 모두 읽어서 연결해줌. 따라서 사용자가 할 일은 없음. 

     

     

    MyBatis 동적 쿼리

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

    MyBatis가 제공하는 최고의 기능이다. 동적 쿼리를 위해 제공되는 기능은 다음과 같다.

     

    if

    <select id="findActiveBlogWithTitleLike"
        resultType="Blog">
        SELECT * FROM BLOG
        WHERE state = ‘ACTIVE’
     <if test="title != null">
        AND title like #{title}
     </if>
    </select>
    • 해당 조건에 따라 값을 추가할지 말지 판단한다.
    • 내부의 문법을 OGNL을 사용한다. 자세한 내용을 ONGL을 검색해보자. 

     

    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>
    • 이 예제의 문제점은 문장을 모두 만족하지 않을 때 발생한다. 
    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>

    1. 문장이 없으면 where를 추가하지 않음. 문장이 있으면 where를 추가함.
    2. 만약 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,6)'과 같은 문장을 쉽게 완성할 수 있다.
    • 파라미터로 List를 전달하면 된다. 

     

     

    어노테이션으로 SQL 작성

    @Select("select id, item_name, price, quantity from item where id=#{id}")
    Optional<Item> findById(Long id);
    • @Insert, @Update, @Delete, @Select 기능이 제공됨. 
    • XML 대신에 어노테이션에 SQL을 작성할 수 있음. 그런데 XML에 작성하는 것이 더 메리트 있기 때문에 잘 사용하지는 않는다. 
    • 동적 SQL이 해결되지 않으므로 간단한 경우에만 사용한다. 
    • 어노테이션으로 SQL을 작성했다면, XML에 작성된 <select ..> 쿼리는 삭제해야함. 

     

    문자열 대체(String Substitution)

    • #{} 문법을 ?를 넣고 파라미터를 바인딩하는 PreparedStatment를 사용한다. 이 경우 파라미터에 있는 값이 들어옴.
    • 문자 그대로 넣고 싶은 경우 ${}를 이용하면 됨.

     

    Select("select * from user where ${column} = #{value}")
    User findByColumn(@Param("column") String column, @Param("value") String
            value);

    ${}를 사용하면 SQL 인젝션 공격을 당할 수 있다. 따라서 가급적 사용하면 안된다. 사용하더라도 매우 주의깊게 사용해야 한다. 

     

    재사용 가능한 SQL 조각

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

    <sql>을 사용하면 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

    결과를 맵핑할 때 테이블은 'user_id' 이지만 객체는 'id'이다. 이 경우 컬럼명과 객체의 프로퍼티 명이 다르다. 보통 별칭 ('as')를 사용해서 해결할 수 있다. 

    // 별칭을 이용해서 해결
    <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>

    별칭을 사용하지 않고도 문제를 해결할 수 있는데, 다음과 같이 'resutlMap'을 선언해서 사용하면 된다 

    // Result Map 선언
    <resultMap id="userResultMap" type="User">
     <id property="id" column="user_id" />
     <result property="username" column="user_name"/>
     <result property="password" column="hashed_password"/>
    </resultMap>
    
    // Result Map 사용
    <select id="selectUsers" resultMap="userResultMap">
        select user_id, user_name, hashed_password
        from some_table
        where id = #{id}
    </select>

     

     

    MyBatis 공식 사이트

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

    'Spring > Spring DB' 카테고리의 다른 글

    스프링 DB : 스프링 데이터 JPA 관련  (0) 2023.01.23
    Spring DB : JPA  (0) 2023.01.23
    Spring DB : DB 테스트  (0) 2022.07.04
    Spring DB : JdbcTemplate  (0) 2022.06.11
    Spring DB : JdbcTemplate을 이용한 반복 문제 해결  (0) 2022.05.19

    댓글

    Designed by JB FACTORY