[Spring DB 2편] 4. MyBatis

HJ·2023년 2월 3일
1

Spring DB 2편

목록 보기
4/11
post-thumbnail
post-custom-banner

김영한 님의 스프링 DB 2편 - 데이터 접근 활용 기술 강의를 보고 작성한 내용입니다.


1. MyBatis 시작하기

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

1-1. 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>
        ...
    </where>
</select>

자바에서 SQL을 작성하는 경우, 문자열을 사용하기 때문에 라인을 나눌 때 + 를 사용해야 하지만, MyBatis 는 쿼리를 XML에 편리하게 작성할 수 있고 라인을 나누기 편리합니다.

또한 JdbcTemplate은 조건문을 사용해서 자바 코드로 직접 동적 쿼리를 작성해야하지만, MyBatis 는 xml 태그를 통해 동적 쿼리를 편리하게 작성할 수 있습니다.


1-2. 의존관계 추가

implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'

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

하지만 MyBatis는 스프링부트가 버전을 관리해주는 공식 라이브러리가 아니기 때문에 뒤에 버전 정보를 붙여주어야 합니다


1-3. 설정 정보 추가

mybatis.configuration.map-underscore-to-camel-case=true
mybatis.type-aliases-package=hello.itemservice.domain

map-underscore-to-camel-case 은 DB 에서 사용하는 언더바를 객체에서 사용하는 카멜 표기법으로 자동 변경해주는 기능입니다. ( 디폴트 false )

이 기능으로 인해 DB 에서 item_name 을 조회해도 객체의 itemName 속성에 값이 정상적으로 들어갑니다. 단, 컬럼이름과 객체이름이 다른 경우라면 조회할 때 별칭을 사용하면 됩니다.

1-1 을 보면 resultType 에 객체가 명시되어 있는 것을 볼 수 있습니다. 이처럼 MyBatis 에서 타입 정보를 사용할 때는 패키지 이름을 적어주어야 하는데 위처럼 type-aliases-package 를 지정하여, 쿼리를 작성한 xml 파일에서 패키지 이름을 생략할 수 있습니다.

패키지 이름을 설정하면 지정한 패키지와 그 하위 패키지가 자동으로 인식되어 패키지 이름 없이 객체 이름만 작성해도 사용할 수 있으며, 여러 위치를 지정하려면 , 또는 ; 으로 구분하면 됩니다.




2. MyBatis 사용하기

2-1. 매퍼 인터페이스

@Mapper
public interface ItemMapper {

    void save(Item item);

    void update(@Param("id") Long id, @Param("updateParam") ItemUpdateDto updateDto);

    Optional<Item> findById(Long id);

    List<Item> findAll(ItemSearchCond itemSearchCond);
}

MyBatis 매핑 XML을 호출해주는 매퍼 인터페이스입니다. 이때 @Mapper 를 붙여주어야 MyBatis에서 인식할 수 있으며, ListOptional 반환도 지원합니다.

파라미터가 한 개인 경우는 상관없지만, 파라미터가 두 개 이상인 경우 @Param 을 붙여야합니다. 이때 사용하는 어노테이션은 ibatis 혹은 mybatis 의 어노테이션을 사용해야 합니다.

xml 태그의 id 속성에 메서드 이름을 적어주면, 매퍼 인터페이스의 메서드 호출 시 해당 id 를 가진 SQL 이 실행되며 결과를 반환합니다.


2-2. XML 매핑 파일

2-2-1. 생성하기

xml 은 자바코드가 아니기 때문에 resources 하위에 생성합니다. 이때 xml 파일의 위치는 매퍼 인터페이스가 위치한 경로와 동일해야 합니다.

예를 들어, ItemMapper 가 hello.itemservice.repository.mybatis 에 있다면, xml 파일은 resources 하위에 hello.itemservice.repository.mybatis 에 위치해야 합니다.

또한 xml 파일의 이름은 매퍼 인터페이스와 동일한 이름을 가져야합니다. 위에서 ItemMapper 인터페이스를 생성했기 때문에 ItemMapper.xml 을 생성하면 됩니다.

만약 원하는 곳에서 xml 파일을 관리하고 싶으면 properties 에 mybatis.mapper-locations=classpath:mapper/**/*.xml 를 추가하면 됩니다.

위의 설정은 resources/mapper 를 포함한 그 하위 폴더에 있는 XML을 XML 매핑 파일로 인식하도록 하며, 설정을 추가한 경우 파일의 이름은 자유롭게 설정할 수 있습니다.


2-2-2. xml 선언

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

xml 파일에서 위처럼 xml 을 선언합니다. 그리고 mapper 태그를 통해 namespace 를 지정할 수 있는데 이때 매퍼 인터페이스의 경로를 포함한 이름이 지정되어야 합니다.


2-2-3. insert

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

SQL 에서 사용한 `#{itemName} 과 같은 파라미터에는 매퍼에서 넘긴 객체의 프로퍼티 이름과 매칭되어, 값이 바인딩됩니다. 위 예시에서는 파라미터로 넘긴 Item 의 itemName 의 값이 들어갑니다.

DB 가 키를 생성해 주는 IDENTITY 전략일 때는 useGeneratedKeys="true" 를 사용해서 키를 자동으로 생성할 수 있습니다. 이때 keyProperty 를 통해 키 속성의 이름을 지정해야 합니다.


2-2-4. update

void update(@Param("id") Long id, @Param("updateParam") ItemUpdateDto updateDto);
<update id="update">
    update item
    set item_name = #{updateParam.itemName},
        price=#{updateParam.price},
        quantity=#{updateParam.quantity}
    where id=#{id}
</update>

매퍼 인터페이스에서 전달하는 파라미터가 한 개라면 위의 insert 문에서 처럼 파라미터 이름 없이 바로 속성 값을 꺼내올 수 있지만, 두 개 이상인 경우 @Param 을 사용해서 이름을 지정하기 때문에 지정된 이름으로 해당 값을 사용해야 합니다.

id 의 경우 지정된 이름이 id 이기 때문에 #{id} 를 사용하였고, updateParam 의 경우 객체이기 때문에 #{updateParam.itemName} 형태로 사용하였습니다.


2-2-5. 단일 데이터 조회

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

조회 쿼리의 경우, 반환타입이 있어야 하기 때문에 resultType 을 지정합니다. 이때 select 의 결과를 resultType 에 지정된 객체로 바로 변환해줍니다.

원래 패키지명까지 함께 적어주어야 하지만 1-3 번 에서 설정 정보에 패키지명을 추가했기 때문에 패지키는 생략해서 사용할 수 있습니다.


2-2-6. 여러 데이터 조회

List<Item> findAll(ItemSearchCond itemSearchCond);
<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>

동적 쿼리를 작성할 때는 where 와 if 태그를 사용합니다. 이때 if 태그의 test 에 조건을 명시하고, SQL 은 and 로 시작하도록 작성합니다.

모든 if 가 실패하면 where 를 만들지 않고, 처음에 성공한 if 는 andwhere 로 변환해줍니다.

주의해야 할 점은 위처럼 <= 를 사용할 때 < 를 태그로 인식하기 때문에 &lt; 를 사용합니다. 만약 기호 자체를 사용하고 싶다면 <![CDATA[ ... ]]> 를 사용하면 됩니다.

CDATA 안에서 사용되는 기호는 태그가 아니라 단순 문자로 인식하게 됩니다. 하지만 태그를 문자로 인식한다는 점으로 인해 <if><where> 가 적용되지 않습니다.




3. Repository 생성

@Repository
@RequiredArgsConstructor
public class MyBatisItemRepository implements ItemRepository {
    
    private final ItemMapper itemMapper;
    ...
}

@Mapper 가 붙으면 자동으로 프록시 기술을 통해 구현체를 만들어서 스프링 빈에 등록합니다.

그래서 이전에 생성한 매퍼 인터페이스가 스프링 빈으로 등록되고, Repository 에서 의존관계 주입을 통해 사용할 수 있습니다. 이때 위의 Repository 는 주입받은 itemMapper 에 위임하는 기능만 수행합니다.




4. 인터페이스 구현체

스프링 빈에는 객체 인스턴스만 등록됩니다. 하지만 Repository 를 보면 ItemMapper 는 인터페이스임에도 주입받아서 사용할 수 있습니다. 이것이 가능한 이유는 MyBatis 스프링 연동 모듈이 자동으로 처리해주기 때문입니다.

4-1. 의존관계 주입 원리

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

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

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

구현체 조회 결과 : itemMapper=class com.sun.proxy.$Proxy66


4-2. 매퍼 구현체

MyBatis 스프링 연동 모듈이 만들어주는 매퍼 인터페이스의 구현체 덕분에 인터페이스만으로 편리하게 XML의 데이터를 찾아서 호출할 수 있습니다.

또 예외 변환까지 처리해주기 때문에 MyBatis 에서 발생한 예외를 스프링 예외 추상화인 DataAccessException 에 맞게 변환해서 반환해줍니다.

참고로 MyBatis 스프링 연동 모듈은 많은 부분을 자동으로 설정해주는데, 데이터베이스 커넥션, 트랜잭션과 관련된 기능도 MyBati s와 함께 연동하고, 동기화해줍니다.




5. 여러 동적 쿼리

5-1. choose

<choose>
    <when test="title != null">
        AND title like #{title}
    </when>

    <otherwise>
        AND featured = 1
    </otherwise>
</choose>

choose 를 사용해서 자바의 switch 구문과 유사한 동적 쿼리를 작성할 수 있습니다. 이때 <when> 을 하나도 만족하지 않으면 <otherwise> 가 실행됩니다.


5-2. foreach

<where>
    <foreach item="item" index="index" collection="list" open="ID in (" separator="," close=")" nullable="true">
        #{item}
    </foreach>
</where>

foreach 는 파라미터로 List 와 같은 컬렉션을 전달했을 때 이를 반복해서 처리해야 하는 경우에 사용합니다.

collection="list" 는 MyBatis 매핑 파일에서 지정한 리스트 객체를 반복하겠다는 것을 의미합니다.

open="ID in (" 은 반복되는 부분의 시작을, separator="," 는 구분자를, close=")" 는 반복되는 부분의 종료를 의미합니다.

결과적으로 해당 동적 쿼리의 결과는 where in( 1, 2, 3 ) 과 같은 형태가 됩니다.




6. 기타 기능

6-1. 어노테이션으로 SQL 작성

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

매퍼 인터페이스에 @Insert , @Update , @Delete , @Select 와 같은 어노테이션을 사용해서 쿼리를 작성할 수 있습니다. 이때 xml 에 있는 쿼리는 제거해야 합니다.

하지만 어노테이션을 사용하면 동적 쿼리가 불가능하기 때문에 간단한 경우에만 사용하는 것이 좋다고 합니다.


6-2. 문자열 대체

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

#{ } 문법은 파라미터 바인딩을 수행하는데, 파라미터 바인딩이 아닌 문자 그대로를 출력하고 싶은 경우에는 ${ } 를 사용합니다.

예를 들어, ORDER BY 에서 사용할 컬럼명을 지정하고 싶은 경우에 사용합니다. 하지만 ${} 를 사용하면 SQL 인젝션 공격을 당할 수 있기 때문에 가급적 사용하면 안된다고 합니다.


6-3. 재사용 가능한 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>

2-2-5 와 2-2-6 을 보면 조회 시 사용되는 컬럼이 동일한 것을 볼 수 있습니다. 이러한 경우 컬럼을 별도로 정의해서 include 를 통해 가져올 수 있습니다.

<sql> 을 사용하면 SQL 코드를 재사용 할 수 있는데 여기에 사용할 부분을 정의하고, <include> 를 통해 <sql> 조각을 찾아서 사용합니다.


6-4. ResultMaps

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

결과를 매핑할 때 테이블의 칼럼명과 객체의 필드명이 다른 경우 별칭 as 를 사용합니다. 예를 들어, user_id as id 처럼 사용할 수 있습니다.

이처럼 별칭을 사용하는 방식 말고 resultMap 을 선언해서 사용할 수 있습니다. select 에 resultType 이 아닌 resultMap 을 지정하고, resultMap 의 type 에 객체를 지정합니다.

result 에는 객체의 필드명과 조회하는 컬럼명을 작성하면 됩니다.

post-custom-banner

0개의 댓글