JDBC로 모든걸 다 하려하니 너무 번거로웠다. 이를 어느정도 해결할 수 있는 수단 중 하나인 MyBatis에 대해 간단히 정리해보겠다.
JDBC에서 SQL의 결과를 우리가 가지고 있는 Java객체에 매핑하려면 ResultSet을 불러와서 값을 직접 객체에 설정해주어야 한다. 즉 무지 귀찮다.
이를 쉽게 해주는 것이 MyBatis이다. MyBatis는 Java객체와 SQL데이터베이스 사이의 매핑을 쉽게 해주는 SQL 매퍼 프레임워크이다. 주의해야 할 건 이후 MyBatis는 완전한 ORM이 아니다.
MyBatis는 직접 작성한 SQL을 통한 결과를 XML이나 Annotation을 통해 수동으로 매핑하는 도구다.
이러한 이유로 MyBatis는 완전한 ORM이 아니다.(수동으로 매핑하므로)
혹자는 MyBatis가 구닥다리 프레임워크라고 할 수 있지만, 상황에 따라 다르다고 볼 수 있다.
특히 SQL 최적화나 복잡한 쿼리 작성이 중요하다면 JPA보다 좋은 선택이 될 수 있다.
이는 사실 Spring Boot에서 제공하는 설정인데 우리는 spring boot로 웹 개발할거니 적극 사용하도록 하자.
application.yml파일을 통해 여러 편리한 설정을 할 수 있는데 대충 아래와 같이 생겼다.
spring:
datasource:
url: jdbc:mysql://localhost:3306/mybatis
username: dev_bae
password:
mybatis:
type-aliases-package: io.moon.mybatistest.entity
configuration:
map-underscore-to-camel-case: true
mapper-locations: classpath:mappers/*.xml
spring.datasource 부분으로 연결정보를 입력하여 자동으로 DB에 접속할 수 있다. JDBC에 비해 아주 깔끔 간단한 모습
mybatis에 여러 설정을 걸 수 있는데
type-aliases-package: XML에서 저 패키지 안의 클래스들은 간단한 이름으로 참조가 가능하다.(패키지명을 줄줄히 안 써도 된다.)configuration.map-underscore-to-camel-cas : SQL결과의 칼럼명이 스네이크케이스(item_id)일 경우 자바객체의 카멜케이스(itemId)로 자동 매핑해준다.mapper-location : 우리가 작성할 XML기반 매퍼파일(SQL이 작성되는 XML파일)의 위치를 지정한다. classpath: 는 resources/폴더를 기준으로 탐색한다.MyBatis에서 mapper.xml에 작성한 SQL 쿼리를 수행하려면, 이를 호출할 메소드를 인터페이스에 정의해두어야 한다.
이때 mapper.xml의 <select>, <insert> 등 태그의 id는 해당 인터페이스의 메소드 이름과 정확히 일치해야 한다.
이 Mapper 인터페이스는 @Mapper 어노테이션을 사용하여 선언할 수 있으며,
해당 어노테이션이 붙은 인터페이스에 대해 MyBatis가 프록시 기반의 구현체를 자동 생성한다.
생성된 구현체는 Spring에서 스프링 빈으로 등록되어 의존성 주입 등을 통해 사용할 수 있다.
🤔 프록시 구현체가 뭐임?
프록시는 자주 듣지않음? 대리인 역할을 하는 객체라고 생각하면 됌. 님 자바에서 인터페이스만 있는데 이걸 구현도 안하고 사용할 줄 앎? 그럼 나도 알려주셈. 안되는거임. 그래서 MyBatis가 @Mapper가 사용된 인터페이스를 찾아서 프록시 구현체를 지가 만들어 주는 거임. 이 프록시 구현체는 인터페이스의 메소드가 호출되면 인터페이스 대신 해당 메소드를 실행함. 내부적으로
java.lang.reflect.Proxy또는 CGLIB 같은 기술을 사용해 이 객체를 생성함. 이건 나도 잘 몰라서 추후에 정리할 예정임.
@Mapper
public interface ItemMapper {
void save(Item item);
int updatePrice(@Param("itemCode") String itemCode, @Param("price") Integer price);
Optional<Item> findByItemCode(@Param("itemCode") String itemCode);
}
뭐 이렇게 생겼는데. @Param 은 예를들어 @Param("itemCode")라면 XML파일의 #{itemCode}부분과 매핑될 매개변수를 지정한다.
매개변수가 하나라면 @Param이 없어도 혼동이 없겠지만 두 개 이상이 바인딩되야 한다면 @Param으로 적확하게 이름을 알려주어야한다. 바인딩이란 어떤 변수나 매개변수에 실제 값을 연결(할당)하는 것을 말한다.
MyBatis에서 SQL매핑의 방식은 어노테이션 매핑방법과 XML기반 매핑방법이 있다. 이글에선 XML기반 매핑만 다루어보겠다.
JDBC에서는 쿼리를 DB에게 날리려면 연결을 따와서 Statement를 가져와서 이를 실행시켜야 했지만 MyBatis에서는 XML에 SQL만 매핑시켜놓고 이를 실행시킬 메소드를 인터페이스에 정의해놓기만하면 해당 쿼리를 DB에 실행시킬 수 있다.
참 편하다.
<?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="io.moon.mybatistest.mybatis.ItemMapper">
<insert id="save" useGeneratedKeys="true" keyProperty="id">
INSERT INTO items
(name, code, price)
VALUES
(#{name}, #{itemCode}, #{price})
</insert>
<update id="updatePrice">
UPDATE
items
SET
price = #{price}
WHERE
code = #{itemCode}
</update>
<select id="findByItemCode" resultType="Item">
SELECT
-- 자바객체의 필드이름을 기준으로 결과를 가져와야한다. as를 통해 이를 수행.
i.item_id as id,
i.name,
i.code as itemCode,
i.price,
i.created_at as createdAt
FROM
items i
WHERE
i.code = #{itemCode}
</select>
</mapper>
저 위 세줄은 그냥 어디선가 가져오길 바란다.
mapper태그 안에 매핑될 SQL을 작성하는 방식이고 이 안의 namespace로 작성한 SQL을 실행할 메소드가 정의되어있는 인터페이스의 경로를 입력한다.
mapper태그 안에 작성할 SQL문에 따라 지정하고(INSERT,UPDATE,SELECT,DELETE) id에는 자바메소드 이름을 적으면 된다. resultType은 반환타입을 정해준다.
useGeneratedKeys은 DB에서 자동 생성된 키(ex. AUTO_INCREMENT)를 자바 객체에 넣어주려면 true로 입력하고,
keyProperty는 이 키값를 자바객체의 어느 필드에 저장할지를 지정한다.
추가로 주의해야할 부분은 SELECT시 DB의 칼럼명과 자바객체의 필드명이 상이하게 다를때(단, 스케이크케이스에서 카멜케이스는 설정하면 자동으로 매핑)
이를 DB에서 가져올때 AS를 통해 객체 필드명과 동일하게 가져와야한다.
위의 개념들을 총동원해서 간단한 예제를 실습해보자.
Product테이블은 id, name, category, price 칼럼을 가진다. 이를 가격 범위로 검색하는 실습을 해보겠다.
우선 DB에 products테이블을 생성하자
create table products(
id int auto_increment primary key,
name varchar(50) not null,
category varchar(100) not null,
price int not null
);
Products Entity클래스를 만들자
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Products {
private int id;
private String name;
private String category;
private int price;
public Products(String name, String category, int price) {
this.name = name;
this.category = category;
this.price = price;
}
@Override
public String toString() {
return "Products{" +
"id=" + id +
", name='" + name + '\'' +
", category='" + category + '\'' +
", price=" + price +
'}';
}
}
본인 이 Entity클래스에 생성자를 만들지 않아 수많은 에러를 맛봐야했음..
MyBatis는 생성자와 Setter를 기반으로 SELECT의 결과를 객체로 만들기 때문에 생성자가 있어야함..
Mapper인터페이스도 만들자
@Mapper
public interface ProductsMapper {
void saveProducts(Products products);
List<Products> searchProductsByPriceRange(
@Param("category") String category,
@Param("min") int min,
@Param("max") int max
);
}
이제 저 @Param에 입력된 값을 잘 기억해서 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="io.moon.mybatistest.mybatis.ProductsMapper">
<insert id="saveProducts" useGeneratedKeys="true" keyProperty="id">
INSERT INTO products
(name, category, price)
VALUES
(#{name}, #{category}, #{price})
</insert>
<select id="searchProductsByPriceRange" resultType="Products">
SELECT * FROM products
WHERE category = #{category}
AND price BETWEEN #{min} AND #{max}
</select>
</mapper>
반환타입이 List<Products>로 설정되어있고(인터페이스) xml의 resultType이 Products이므로 MyBatis가 결과값을 List에 넣어준다.
즉 각 행이 Products의 객체로 매핑된다.
이제 검색을 하면 되겠지만 테이블에 검색할 데이터가 없다. 이를 간단히 채울 수 있는 메소드를 만들어봤다.
@Component
public class ProductsUtil {
@Autowired
private ProductsMapper productsMapper;
public ProductsUtil(ProductsMapper productsMapper) {
this.productsMapper = productsMapper;
}
ArrayList<Products> productsList = new ArrayList<>();
List<String> categories = List.of("Food", "Beverage", "Snack");
public List<Products> genProducts(int quantity) {
for (int i = 0; i < quantity; i++) {
String name = "product" + i;
Products products = new Products(name, categories.get(i%3), (int) (Math.random() * 10000));
productsMapper.saveProducts(products);
productsList.add(products);
}
return productsList;
}
}
Food, Beverage, Snack카테고리를 가진 랜덤한 가격을가진 값들을 테이블에 INSERT하는 역할이다.

잘 작동하는 모습
마지막으로 카테고리별 금액 범위 검색을 해보면
@Test
@DisplayName("searchProducts")
void searchProducts() throws Exception {
List<Products> results = mapper.searchProductsByPriceRange("Food",2000,6000);
for (Products products : results) {
log.info("products = {}", products);
}
}

아주 잘 작동한다.
이처럼 MyBatis는 비교적 복잡한 쿼리가 많다면 JPA보다 좋은 선택이 될 수도? 있다.