-
데이터 접근 기술- MyBatisBackEnd/Spring DB 2023. 2. 2. 07:00반응형
MyBatis는 기본적으로 JdbcTemplate이 제공하는 대부분의 기능을 제공하는 SQL Mapper입니다. JdbcTemplate과 비교해서 가장 매력적인 점은 SQL을 XML에 편리하게 작성할 수 있고 동적 쿼리를 매우 편리하게 작성할 수 있다는 점입니다. 다만, JdbcTemplate은 스프링에 내장된 기능이라 별도의 설정없이 사용할 수 있지만 MyBatis는 약간의 설정이 필요합니다. 프로젝트에서 동적 쿼리와 복잡한 쿼리가 많다면 MyBatis를 사용하고, 단순한 쿼리들이 많으면 JdbcTemplate을 선택해서 사용하면 됩니다.
MyBatis 설정
의존성 주입(build.gradle)
MyBatis는 스프링 부트가 버전을 관리해주는 공식 라이브러리가 아니기에 버전 정보를 붙여야 합니다.
//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.type-aliases-package: MyBatis에서 타입 정보를 사용할 때는 이름을 적어주어야 합니다. 설정 파일에 명시하면 패키지 이름을 생략할 수 있습니다. 지정한 패키지와 그 하위 패키지가 자동으로 인식됩니다. 여러 위치를 지정하려면 , or ;로 구분하면 됩니다.
- mybatis.configuration.map-underscore-to-camel-case: 자바 객체에서는 주로 카멜(camel) 표기법을 사용하고, 관계형 데이터베이스에서는 주로 언더스코어를 사용하는 snake_case 표기법을 사용합니다. map-underscore-to-camel-case 기능을 활성화 하면 언더스코어 표기법을 카멜로 자동 변환해줍니다. 따라서 데이터베이스에서 item_name으로 조회해도 객체의 itemName 속성에 값이 정상 입력됩니다. 컬럼 이름과 객체 이름이 완전히 다른 경우에는 조회 SQL에서 별칭(alias)을 사용하면 됩니다(예: select item_name as name from item).
- logging.level.hello.itemservice.repository.mybatis=trace: MyBatis에서 실행되는 쿼리 로그를 확인할 수 있습니다.
MyBatis 적용
MyBatis 매핑 XML을 호출해주는 매퍼 인터페이스를 작성합니다. 해당 인터페이스에는 @Mapper 애노테이션을 붙여주어야 MyBatis에서 인식할 수 있습니다.
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 org.apache.ibatis.annotations.Select; 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); }
해당 인터페이스의 메서드를 호출하면 다음에 보이는 xml의 해당 SQL을 실행하고 결과를 돌려줍니다. 자바 코드가 아니기 때문에 src/main/resources 하위에 만들되, 패키지 위치는 맞추어 주어야 합니다. xml 파일 namespace에 앞서 만든 매퍼 인터페이스를 지정하면 됩니다.
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 <= #{maxPrice} </if> </where> </select> </mapper>
save
- Insert SQL은 <insert>를 사용하면 됩니다.
- id에는 매퍼 인터페이스에 설정한 메서드 이름을 지정하면 됩니다.
- 파라미터는 #{}문법을 사용하여 매퍼에서 넘긴 객체의 프로퍼티 이름을 적어주면 됩니다. 문자 그대로를 처리하고 싶은 경우, ${}를 사용하면 됩니다. 단, ${}의 경우 SQL 인젝션 공격을 당할 수 있기에 가급적 사용하면 안 됩니다.
- useGeneratedKeys는 데이터베이스가 키를 생성해 주는 IDENTITY 전략일 때 사용합니다. KeyProperty는 생성되는 키의 속성 이름을 지정합니다. Insert가 끝나면 item 객체의 id속성에 생성된 값이 입력됩니다.
update
- Update SQL은 <update>를 사용하면 됩니다.
- 파라미터가 2개 이상이면 매퍼 인터페이스에 @Param으로 이름을 지정해서 파라미터를 구분해야 합니다.
select
- Select SQL은 <select>를 사용하면 됩니다.
- resultType은 반환타입을 명시하면 됩니다. application.properties 내 mybatis.type-aliases-package 설정으로 모든 패키지명을 다 적지 않아도 되며, mybatis.configuration.map-underscore-to-camel-case 설정으로 언더스코어를 카멜 표기법으로 자동 변환해줍니다.
- MyBatis는 <where>, <if> 같은 동적 쿼리 문법을 제공합니다. <if>는 해당 조건이 만족하면 구문을 추가하고 <where>은 적절하게 where 문장을 만들어줍니다. 예제에서 <if>가 모두 실패 시 where 문장을 만들지 않으며, <if>가 하나라도 성공하면 처음 나타나는 and를 where로 변환해줍니다.
XML 특수문자
XML에서 태그가 시작하거나 종료할 때 <, >와 같은 특수문자를 사용하기에 다음과 같이 치환해서 사용해야 합니다.
< : < > : > & : &
다른 해결 방안으로는 XML에서 지원하는 CDATA 구문 문법을 사용하는 것입니다.
<if test="maxPrice != null"> <![CDATA[ and price <= #{maxPrice} ]]> </if>
Note. XML 파일을 원하는 위치에 두고 싶으면 application.properties에 다음과 같이 설정하면 됩니다. resources/mapper를 포함한 그 하위 폴더에 있는 XML을 XML매핑 파일로 인식합니다.
mybatis.mapper-locations=classpath:mapper/**/*.xml
다음으로 ItemRepository 구현체를 만듭니다. MyBatisItemRepository는 단순히 ItemMapper에 기능을 위임합니다.
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 lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Repository; import java.util.List; import java.util.Optional; @Slf4j @Repository @RequiredArgsConstructor public class MyBatisItemRepository implements ItemRepository { private final ItemMapper itemMapper; @Override public Item save(Item item) { log.info("itemMapper class={}", itemMapper.getClass()); 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); } }
신규 Config 파일을 생성하고 ItemServiceApplication에 해당 Config 파일로 설정합니다.
package hello.itemservice.config; import hello.itemservice.repository.ItemRepository; import hello.itemservice.repository.jdbctemplate.JdbcTemplateItemRepositoryV3; 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); } }
@Import(MyBatisConfig.class) @SpringBootApplication(scanBasePackages = "hello.itemservice.web") public class ItemServiceApplication {...}
MyBatis 동작 원리
- 애플리케이션 로딩 시점에 MyBatis 스프링 연동 모듈은 @Mapper가 붙어있는 인터페이스를 조사합니다.
- 해당 인터페이스가 발견되면 동적 프록시 기술을 사용해서 ItemMapper 인터페이스의 구현체를 만듭니다.
- 생성된 구현체를 스프링 빈으로 등록합니다.
- 매퍼 구현체 덕분에 MyBatis를 스프링에 편리하게 통합해서 사용할 수 있습니다.
- 매퍼 구현체를 사용하면 스프링 예외 추상화도 함께 적용됩니다.
- MyBatis 스프링 연동 모듈이 많은 부분을 자동으로 설정해주는데, 데이터베이스 커넥션, 트랜잭션과 관련되 기능도 MyBatis와 함께 연동하고 동기화 해줍니다.
MyBatis 동적 쿼리
- if
- choose (when, otherwise)
- trim (where, set)
- foreach
if
해당 조건에 따라 값을 추가할지 말지 판단합니다. 내부의 문법은 OGNL을 사용합니다.
<select id="findActiveBlogWithTitleLike" resultType="Blog"> SELECT * FROM BLOG WHERE state = ‘ACTIVE’ <if test="title != null"> AND title like #{title} </if> </select>
choose, when, otherwise
자바의 switch 구문과 유사한 구문도 사용할 수 있습니다.
<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>
where
<where>는 만족하는 <if>가 없는 경우 where를 추가하지 않습니다. 만족하는 <if>가 and로 시작하면 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>
trim
다음과 같이 <where>와 같은 기능을 수행할 수 있습니다.
<trim prefix="WHERE" prefixOverrides="AND |OR "> ... </trim>
foreach
컬렉션을 반복 처리할 때 사용합니다. where in (1,2,3,4,5)와 같은 문장을 쉽게 완성할 수 있습니다. 파라미터로 List를 전달하면 됩니다.
<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>
동적 쿼리에 대한 자세한 내용은 공식 메뉴얼을 참고하시면 됩니다.
MyBatis 애노테이션
다음과 같이 XML 대신에 애노테이션에 SQL을 작성할 수 있습니다. @Insert, @Update, @Delete, @Select 기능이 제공되며, 애노테이션에 작성 시 XML에서는 제거해야 합니다. 동적 SQL이 해결되지 않으므로 간단한 경우에만 사용합니다.
@Select("select id, item_name, price, quantity from item where id=#{id}") Optional<Item> findById(Long id);
애노테이션으로 SQL 작성에 대한 자세한 내용은 공식 메뉴얼을 참고하시면 됩니다.
다음과 같은 기능들도 제공하며 자세한 내용은 공식 메뉴얼을 참고하시면 됩니다.
- cache - 해당 네임스페이스을 위한 캐시 설정
- cache-ref - 다른 네임스페이스의 캐시 설정에 대한 참조
- resultMap - 데이터베이스 결과데이터를 객체에 로드하는 방법을 정의하는 엘리먼트
parameterMap - 비권장됨! 예전에 파라미터를 매핑하기 위해 사용되었으나 현재는 사용하지 않음- sql - 다른 구문에서 재사용하기 위한 SQL 조각
[참고 정보]
반응형'BackEnd > Spring DB' 카테고리의 다른 글
데이터 접근 기술- Spring Data JPA (0) 2023.02.04 데이터 접근 기술- JPA (2) 2023.02.04 데이터 접근 기술- 스프링 JdbcTemplate (0) 2023.02.01 데이터 접근 기술- Memory (0) 2023.01.31 JdbcTemplate (0) 2023.01.28