- Today
- Total
개발하는 고라니
[Spring Boot] MyBatis 본문
Java를 이용해 웹 개발을 한다면 Spring Boot를 높은 확률로 사용할 것이고, 데이터베이스와 연결하기 위해 MyBatis나 JPA를 많이 사용하는 추세이다. 만약 Spring Boot와 MyBatis를 사용할 경우 간단한 설정과 사용법을 보도록 하자
MyBatis
먼저 이를 사용하려면 라이브러리가 필요한데, Maven Repository에서 가져와 인젝션 해주자. 나는 MySQL을 사용할 것 이기에 mysql 라이브러리도 가져왔다. 그리고 jdbc는 필수로 있어야 한다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.24</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
이제 MVC2 패턴으로 프로젝트를 구성하는데, DBMS와 연동하는 곳을 DAO 혹은 Repository라고 한다. 나는 Repository라고 칭하도록 하겠다. 인터페이스는 어떤 기술에 종속되는 것은 바람직하지 않으므로 XML로 따로 뺸다.
MySQL 연동
연동하는 방법은 application-properties에서 몇줄 적어주면 끝난다.
//applictaion.properties
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://hi.namoolab.com:0000
spring.datasource.username=[ ]
spring.datasource.password=[ ]
Mapper - Java
import com.newlec.web.entity.Notice;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface NoticeRepository {
@Select("SELECT * FROM Notice WHERE ${field} LIKE '%${query}%'")
List<Notice> getList(int page, String field, String query);
}
위처럼 인터페이스 단에 @Mapper만 붙여주면 스프링의 빈으로 등록되면서 이 인터페이스를 구현한 인스턴스가 저절로 생기게 되며 스프링이 그것을 관리하고 사용한다. @Select 안에 ${}와 #{}가 있는데, 이 둘의 차이점은
${ }는 Key를 나타내는 것으로써 '' 같은 싱글 쿼테이션이 들어가지 않으며,
#{ }는 Value를 나타내는 것으로 싱글쿼테이션으로 감싸진다.
이렇게 NoticeRepository를 만들어보았다. 기능은 목록을 가져오는 기능 하나밖에 없다. 그도 그럴 것이, 이 인터페이스에서 MyBatis를 사용하지 않을 것이기 때문이다. 인터페이스는 어떤 기술에 종속되는 것이 바람직하지 않으므로 XML로 잠시 후에 뺄 것이다.
이렇게 XML로 뺴는 것의 장점은 인터페이스를 좀 더 인터페이스 답게 정의만 한다.
Mapper - XML
여기부터가 중요하다. 실제 MyBatis는 자바로 풀어내는 것보다 XML로 따로 뺴서 구현하는 것이 더 바람직하며 대부분 이 방법을 사용한다.
이제 Java 클래스에서 만든 Mapper를 XML로 만드는 것을 해보자. 우선 MyBatis 에서 레퍼런스 문서를 찾았더니 다음과 같이 만든다고 나와있다.
(출처 : https://mybatis.org/mybatis-3/ko/configuration.html#typeHandlers)
<?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="org.apache.ibatis.submitted.rounding.Mapper">
<resultMap type="org.apache.ibatis.submitted.rounding.User" id="usermap">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="funkyNumber" property="funkyNumber"/>
<result column="roundingMode" property="roundingMode"/>
</resultMap>
<select id="getUser" resultMap="usermap">
select * from users
</select>
<insert id="insert">
insert into users (id, name, funkyNumber, roundingMode) values (
#{id}, #{name}, #{funkyNumber}, #{roundingMode}
)
</insert>
<resultMap type="org.apache.ibatis.submitted.rounding.User" id="usermap2">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="funkyNumber" property="funkyNumber"/>
<result column="roundingMode" property="roundingMode" typeHandler="org.apache.ibatis.type.EnumTypeHandler"/>
</resultMap>
<select id="getUser2" resultMap="usermap2">
select * from users2
</select>
<insert id="insert2">
insert into users2 (id, name, funkyNumber, roundingMode) values (
#{id}, #{name}, #{funkyNumber}, #{roundingMode, typeHandler=org.apache.ibatis.type.EnumTypeHandler}
)
</insert>
</mapper>
import com.newlec.web.entity.Notice;
import java.util.List;
public interface NoticeRepository {
List<Notice> getList();
List<Notice> getList(int page);
List<Notice> getList(int page, String field, String query);
Notice get(int id);
int insert(Notice notice);
int update(Notice notice);
int delete(int id);
}
이제 DAO/Repository의 인터페이스 부분은 깔끔하게 정리가 되었다.
그럼 getList() 메서드를 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="com.newlec.web.repository.NoticeRepository">
<select id="getList" resultType="com.newlec.web.entity.Notice">
"SELECT * FROM Notice WHERE ${field} LIKE '%${query}%'"
</select>
</mapper>
<!--
namespace : 해당 패키지안의 클래스에 정의되어있다.
id : 인터페이스에 정의한 메서드명
resultType : 반환 타입
-->
구현체
그런데, 위에서 Java로 했을 때는 @Mapper 어노테이션을 붙여주면 스프링이 Mapper의 구현체를 만들어주었는데, XML로 뺐으면 이에 대한 구현체는 우리가 직접 만들어주어야 한다. MyBatis는 사실 구현을 도와주는 것이지 구현을 해주는 녀석이 아니기 때문이다.
사전 지식으로 알아야할 것이 Spring에게 IoC Container가 있듯. MyBatis에게도 Mapper Container(Mapper Register)라는 상자가 있다. 그러므로 MyBatis는 @Mapper가 붙은 자바코드나, xxxMapper.xml을 상자에 담을 수 있는 것이다.
그럼 먼저 MyBatis에게 xxxMapper.xml 파일이 어디에 있으니 그곳에서 가져가라는 말을 해주어야 한다. Spring을 쓰면 root-context.xml에서 설정을 하겠지만, 지금은 Spring Boot를 사용하니 설정 방법은 boot에 맞추어서 한다.
application.properties 설정파일에
mybatis.mapper-locations=classpath:com/newlec/web/repository/mybatis/mapper/*Mapper.xml
한 줄을 추가해준다. 경로는 반드시 저래야할 필요는 없으며, 각자 사용자에 맞춰 Mapper.xml을 보관해둔 곳을 명시해주면 된다.
SqlSession
위의 작업까지 마치면 MyBatis의 Mapper Container에 Mapper 객체들이 담기게 된다. 이제 남은 것은 이 객체들을 Spring의 IoC Container에 담아주는 작업이다.
그러기 위해선 SqlSession이라는 도구의 도움이 필요하다. 이 도구는 Dao/Repository를 구현하기 위해 Mapper 정보를 가져와주는 녀석이다.
전체적인 흐름을 도식화하면 다음과 같다.
NoticeRepository(interface)
|
XML --> Mapper 컨테이너에 담김
|
SqlSession의 도움 --> Mapper 컨테이너에서 특정 Mapper를 가져옴
|
MyBatisNoticeRepository(class) --> IoC 컨테이너에 담김
참고) IntelliJ에서 XML 파일
이것때문에 한참을 고생했다. 인텔리제이는 src/main/java 밑의 .xml파일을 읽지 못해 resources 밑에 Mapper.xml 경로를 설정과 동일하게 생성해서 그곳에 넣어줘야 한다. 하지만 이것이 싫다면 Maven을 사용한다는 가정하에 pom.xml의 <build> 밑에 다음과 같은 문구를 넣어주면 된다.
<build>
...
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
</build>
ResultMap
resultMap은 컬럼을 맞춰주는 용도로 많이 쓰인다. 만일 DB 테이블에서 컬럼명이 Snake case(예: writer_id) 인 경우, 자바에서는 Camel Case가 명명 규칙이므로 자바에서의 필드명과 테이블의 컬럼명이 다를 수 있다. 이럴 경우 매핑이 안되므로 resultMap을 사용해 writer_id를 writerId로 매핑해주는 작업이 필요하다.
<result>태그의 column은 테이블의 컬럼명을 적고, property에 XML에서 인식할 값을 적는다.
<resultMap type="com.newlecture.web.entity.Notice" id="noticeMap">
<result column="writer_id" property="writerId"/>
<result column="reg_date" property="regDate"/>
</resultMap>
동적 쿼리
동적 쿼리를 하기 전에 MySQL의 페이징 쿼리를 보자.
SELECT * FROM Notice LIMIT 10 OFFSET 20;
맨 처음거부터 20개를 건너뛰고, 거기서부터 10개를 조회해달라는 쿼리이다. MySQL은 limit과 offset을 가지고 간단하게 페이징 처리를 할 수 있다.
Mybatis의 동적쿼리에는 다음과 같은 4가지 태그가 있다.
- if
- choose(when, otherwise)
- trim(where, set)
- foreach
# MyBatis 동적쿼리 docs
if
JSP를 이용해 웹 개발을 해봤다면 JSTL을 알 것인데, JSTL의 문법과 상당히 유사하다. ""안에 조건식을 써서 true면 <if>안의 내용이 실행되게끔 한다.
<!--
<if test="조건식">
...
</if>
-->
<select id="getList" resultType="com.newlecture.web.entity.Notice">
select * from Notice
<if test="field != null">
where ${field} like '%${query}%'
</if>
order by regdate desc
LIMIT #{limit} OFFSET #{offset}
</select>
<select id="findActiveBlogLike" resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<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>
if는 생각보다 간단하게 해결할 수 있다. 하지만 where 절에 AND나 OR가 들어가야한다면, 어떻게 해결해야할까? 그것은 밑에서 풀어내겠다.
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>
위와 같이 where절에 and로 연결된, 2개 이상의 if가 들어갔다고 했을 때, 3개중 앞의 if가 false일 때 sql은 다음과 같이 된다.
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE
AND title like #{title}
AND author_name like #{author.name}
</select>
이는 SQL구문 에러이다. WHERE 뒤에 바로 AND가 올 수는 없다. 이런 경우 <where>을 사용한다. <where>를 쓰면 where 밑의 조건 중 하나만 참이어도 where로 감싸주고, 만약 모든 것이 false면 where은 들어가지 않는다.
게다가 첫번째 if가 false면 'AND title like #{title}'가 where뒤에 올텐데, 위에서 말했듯 where 뒤에 and가 오면 구문 에러인데, 이 또한 처리해준다. 즉 WHERE 뒤에 바로 AND가 오면 이를 떼어준다.
>> 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>
>> trim
그런데 만약 <where>태그가 and라던지 or 등을 제대로 지워주는 것이 안된다면, <trim>을 사용해 해결할 수 있다.
//trim
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
<trim prefix="where" prefixOverrides="and | or">
<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>
</trim>
</select>
>> set
Update시 아래와 같이 할 경우, 파라미터로 들어온 것이 모두 null이 아니어야 한다. 하지만 파라미터로 Notice객체를 넘길 때 바꾸고 싶은 값만 있을 땐 아래와 같이 하면 에러가 발생할 확률이 높다. 왜냐하면 null이 올 수 없는 곳에 null이 들어갈 수도 있고, 잘 있던 값이 갑자기 null로 바뀔수도 있기 때문이다. 이 때 필요한게 <set>태그이다.
<update id="update" parameterType="com.newlecture.web.entity.Notice">
update Notice
SET
title = #{title},
writerId = #{writerId},
content = #{content},
hit = ${hit},
pub = #{pub},
files = #{files}
where id = #{id}
</update>
# 기본 타입(Primitive Type)은 값을 지정하지 않아도 기본 값이 들어간다.
<set>을 적용한 MyBatis
<update id="update" parameterType="com.newlecture.web.entity.Notice">
update Notice
<set>
<if test="title != null">title = #{title},</if>
<if test="writerId != null">writerId = #{writerId},</if>
<if test="content != null">content = #{content},</if>
<if test="hit != null">hit = ${hit},</if>
<if test="pub != null">pub = #{pub},</if>
<if test="files != null">files = #{files}</if>
</set>
where id = #{id}
</update>
>> foreach
<select id="selectPostIn" resultType="domain.blog.Post">
SELECT *
FROM POST P
WHERE ID in
<foreach item="item" index="index" collection="list"
open="(" separator="," close=")">
#{item}
</foreach>
</select>
/*
item : collection으로 들어온 하나하나에 붙여줄 이름
collection : 인자로 들어오는 변수 명( List나 Array 객체만 들어올 수 있다)
index : 인덱스
open : 시작되는 부분에 추가되는 문자
seperator : 각 item 사이의 구분자
close : 끝나는 부분에 추가되는 문자
*/
'Framework > Spring Boot' 카테고리의 다른 글
[Spring Boot] WebSocket과 채팅 (4) - RabbitMQ (13) | 2021.07.30 |
---|---|
[Spring Boot] Thymeleaf (3) | 2021.06.12 |
[Spring Boot] Command 에서 Build 및 실행 (0) | 2021.05.14 |
[Spring Boot] WebSocket과 채팅 (3) - STOMP (68) | 2021.04.09 |
[Spring Boot] WebSocket과 채팅 (2) - SockJS (13) | 2021.04.04 |