-
[김영한 스프링] 11. MyBatis - 적용Spring/스프링 DB 2편 - 데이터 접근 활용 기술 2024. 1. 9. 21:50
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); List<Item> findAll(ItemSearchCond itemSearch); Optional<Item> findById(Long id); }
main/java/hello/itemservice/repository/mybatis/ItemMapper 생성
- 마이바티스 매핑 XML을 호출해 주는 매퍼 인터페이스이다.
- 이 인터페이스에는 @Mapper 애노테이션을 붙여주어야 한다. 그래야 MyBatis에서 인식할 수 있다.
- 이 인터페이스의 메서드를 호출하면 다음에 보이는 xml의 해당 SQL을 실행하고 결과를 돌려준다.
- ItemMapper 인터페이스의 구현체에 대한 부분은 뒤에 별도로 설명한다.
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>
main/resources/hello/itemservice/repository/mybatis/ItemMapper.xml 생성
- namespace : 앞서 만든 매퍼 인터페이스를 지정하면 된다.
- 주의! 경로와 파일 이름에 주의하자.
참고 - XML 파일 경로 수정하기
XML 파일을 원하는 위치에 두고 싶으면 application.properties에 다음과 같이 설정하면 된다.
mybatis.mapper-locations=classpath:mapper/**/*.xml
이렇게 하면 resources/mapper를 포함한 그 하위 폴더에 있는 XML을 XML 매핑 파일로 인식한다. 이 경우 파일 이름은 자유롭게 설정해도 된다.
참고로 테스트의 application.properties 파일도 함께 수정해야 테스트를 실행할 때 인식할 수 있다.insert - save
void save(Item item); <insert id="save" useGeneratedKeys="true" keyProperty="id"> insert into item (item_name, price, quantity) values (#{itemName}, #{price}, #{quantity}) </insert>
- Insert SQL은 <insert>를 사용하면 된다.
- id에는 매퍼 인터페이스에 설정한 메서드 이름을 지정하면 된다. 여기서는 메서드 이름이 save() 이므로 save로 지정하면 된다.
- 파라미터는 #{} 문법을 사용하면 된다. 그리고 매퍼에서 넘긴 객체의 프로퍼티 이름을 적어주면 된다.
- #{} 문법을 사용하면 PreparedStatement를 사용한다. JDBC의 ?를 치환한다 생각하면 된다.
- useGeneratedKeys는 데이터베이스가 키를 생성해 주는 IDENTITY 전략일 때 사용한다. keyProperty는 생성되는 키의 속성 이름을 지정한다. Insert가 끝나면 item 객체의 id 속성에 생성된 값이 입력된다.
update - update
import org.apache.ibatis.annotations.Param; void update(@Param("id") Long id, @Param("updateParam") ItemUpdateDto updateParam); <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개이다. 파라미터가 1개만 있으면 @Param을 지정하지 않아도 되지만, 파라미터가 2개 이상이면 @Param으로 이름을 지정해서 파라미터를 구분해야 한다.
select - findById
Optional<Item> findById(Long id); <select id="findById" resultType="Item"> select id, item_name, price, quantity from item where id = #{id} </select>
- Select SQL은 <select>를 사용하면 된다.
- resultType은 반환 타입을 명시하면 된다. 여기서는 결과를 Item 객체에 매핑한다.
- 앞서 application.properties에 mybatis.type-aliasespackage=hello.itemservice.domain 속성을 지정한 덕분에 모든 패키지 명을 다 적지는 않아도 된다. 그렇지 않으면 모든 패키지 명을 다 적어야 한다.
- JdbcTemplate의 BeanPropertyRowMapper 처럼 SELECT SQL의 결과를 편리하게 객체로 바로 변환해 준다.
- mybatis.configuration.map-underscore-to-camel-case=true 속성을 지정한 덕분에 언더스코어를 카멜 표기법으로 자동으로 처리해 준다. (item_name -> itemName)
- 자바 코드에서 반환 객체가 하나이면 Item, Optional<Item>과 같이 사용하면 되고, 반환 객체가 하나 이상이면 컬렉션을 사용하면 된다. 주로 List를 사용한다. 다음을 참고하자.
select - findAll
List<Item> findAll(ItemSearchCond itemSearch); <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>
- Mybatis는 <where>, <if> 같은 동적 쿼리 문법을 통해 편리한 동적 쿼리를 지원한다.
- <if>는 해당 조건이 만족하면 구문을 추가한다.
- <where>은 적절하게 where 문장을 만들어준다.
- 예제에서 <if>가 모두 실패하게 되면 SQL where를 만들지 않는다.
- 예제에서 <if>가 하나라도 성공하면 처음 나타나는 and를 where로 변환해 준다.
XML 특수문자
그런데 가격을 비교하는 조건을 보자
and price <= #{maxPrice}
여기에 보면 <=를 사용하지 않고 <=를 사용한 것을 확인할 수 있다. 그 이유는 XML에서는 데이터 영역에 <, > 같은 특수 문자를 사용할 수 없기 때문이다. 이유는 간단한데, XML에서 TAG가 시작하거나 종료할 때 <, > 와 같은 특수문자를 사용하기 때문이다.
< : <
> : >
& : &다른 해결 방안으로는 XML에서 지원하는 CDATA 구문 문법을 사용하는 것이다. 이 구문 안에서는 특수문자를 사용할 수 있다. 대신 이 구문 안에서는 XML TAG가 단순 문자로 인식되기 때문에 <if>, <where> 등이 적용되지 않는다.
XML CDATA 사용
<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"> <![CDATA[ and price <= #{maxPrice} ]]> </if> </where> </select>
특수문자와 CDATA 각각 상황에 따른 장단점이 있으므로 원하는 방법을 그때그때 선택하면 된다.
MyBatis 적용2 - 설정과 실행
MyBatisItemRepositorypackage 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); } }
main/java/hello/itemservice/repository/mybatis/MyBatisItemRepository 생성
- MyBatisItemRepository는 단순히 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); } }
- MyBatisConfig는 ItemMapper를 주입받고, 필요한 의존관계를 만든다.
ItemServiceApplication - 변경
package hello.itemservice; import hello.itemservice.config.*; import hello.itemservice.repository.ItemRepository; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Profile; @Slf4j //@Import(JdbcTemplateV3Config.class) @Import(MyBatisConfig.class) @SpringBootApplication(scanBasePackages = "hello.itemservice.web") public class ItemServiceApplication { public static void main(String[] args) { SpringApplication.run(ItemServiceApplication.class, args); } @Bean @Profile("local") public TestDataInit testDataInit(ItemRepository itemRepository) { return new TestDataInit(itemRepository); } }
- @Import(MyBatisConfig.class) : 앞서 설정한 MyBatisConfig.class를 사용하도록 설정했다.
실행
MyBatis 적용3 - 분석
생각해 보면 지금까지 진행한 내용 중에 약간 이상한 부분이 있다.
ItemMapper 매퍼 인터페이스의 구현체가 없는데 어떻게 동작한 것일까?
ItemMapper 인터페이스
@Mapper public interface ItemMapper { void save(Item item); void update(@Param("id") Long id, @Param("updateParam") ItemUpdateDto updateParam); List<Item> findAll(ItemSearchCond itemSearch); Optional<Item> findById(Long id); }
이 부분은 MyBatis 스프링 연동 모듈에서 자동으로 처리해 주는데 다음과 같다.
설정 원리
- 애플리케이션 로딩 시점에 MyBatis 스프링 연동 모듈은 @Mapper가 붙어있는 인터페이스를 조사한다.
- 해당 인터페이스가 발견되면 동적 프록시 기술을 사용해서 ItemMapper 인터페이스의 구현체를 만든다.
- 생성된 구현체를 스프링 빈으로 등록한다.
실제 동적 프록시 기술이 사용되었는지 간단히 확인해 보자.
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 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); } }
실행
JDK 동적 프록시가 적용된 것을 확인할 수 있다.
매퍼 구현체
- 마이바티스 스프링 연동 모듈이 만들어주는 ItemMapper의 구현체 덕분에 인터페이스 만으로 편리하게 XML의 데이터를 찾아서 호출할 수 있다.
- 원래 마이바티스를 사용하려면 더 번잡한 코드를 거쳐야 하는데, 이런 부분을 인터페이스 하나로 매우 깔끔하고 편리하게 사용할 수 있다.
- 매퍼 구현체는 예외 변환까지 처리해 준다. MyBatis에서 발생한 예외를 스프링 예외 추상화인 DataAccessException에 맞게 변환해서 반환해 준다. JdbcTemplate이 제공하는 예외 변환 기능을 여기서도 제공한다고 이해하면 된다.
정리
- 매퍼 구현체 덕분에 마이바티스를 스프링에 편리하게 통합해서 사용할 수 있다.
- 매퍼 구현체를 사용하면 스프링 예외 추상화도 함께 적용된다.
- 마이바티스 스프링 연동 모듈이 많은 부분을 자동으로 설정해 주는데, 데이터베이스 커넥션, 트랜잭션과 관련된 기능도 마이바티스와 함께 연동하고, 동기화해 준다.
참고
마이바티스 스프링 연동 모듈이 자동으로 등록해 주는 부분은 MybatisAutoConfiguration 클래스를 참고하자.출처 : https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2/dashboard
'Spring > 스프링 DB 2편 - 데이터 접근 활용 기술' 카테고리의 다른 글
[김영한 스프링] 12. MyBatis - 기능 정리 (0) 2024.01.09 [김영한 스프링] 10. MyBatis - 소개 & 설정 (0) 2024.01.09 [김영한 스프링] 09. 데이터 접근 기술(테스트) - 임베디드 모드 DB & 스프링 부트와 임베디드 모드 (0) 2024.01.09 [김영한 스프링] 08. 데이터 접근 기술(테스트) - 데이터 롤백 & @Transactional (0) 2024.01.09 [김영한 스프링] 07. 데이터 접근 기술(테스트) - 데이터베이스 연동 & 분리 (0) 2024.01.09