1. Mybatis란?


Spring 카테고리에 글을 작성하지만 MyBatis는 Spring과 전혀 상관없는 독립적인 프레임워크라는걸 미리 말씀드립니다.

또한, MyBatis는 Java에 국한된 프레임워크가 아닙니다. C#, Ruby 등의 언어에서도 사용이 가능합니다.(저는 Java로만 써봤습니다 😅 )

MyBatis란 DAO 객체와 SQL문을 Mapping해주는 Persistence Framework입니다. (SQL Mapper라고도 부릅니다.)

즉, Data Access Layer에 속하는 프레임워크입니다.

 

혹시 JDBC를 이용해 DAO 객체를 만들어 보셨나요?

JDBC 코드를 짜보신 분들은 아시겠지만, CRUD 메서드에서 80~90%는 중복 코드입니다. (정확한 수치는 아닙니다.. ㅎ)

  1. DriverManager로부터 Connection을 받아온다.
  2. Connection 객체로부터 PreparedStatement 객체를 생성한다.
  3. query를 세팅한 후, pstmt.executeQuery() 또는 pstmt.executeUpdate() 메서드를 실행한다.
  4. 결과값을 ResultSet 혹은 int에 담아온다.

 Mybatis의 특징은 크게 2가지입니다.

  1. 위의 JDBC만을 사용했을 때의 불필요한 과정을 걷어내 소스가 깔끔해집니다.
  2. Java코드와 SQL을 분리합니다. <---- 이게 핵심적인 특징입니다 !
    첫째, SQL이 Java코드 안에 들어가있으면 쿼리를 수정했을 때 앱을 새로 컴파일해야하지만, 쿼리를 XML에 넣어놓으면 쿼리를 수정해도 새로 컴파일하지 않아도 됩니다.
    둘째, 협업할 때 DBA분이 SQL을 점검하기 쉽습니다.

아래 보시는거처럼 MyBatis는 DAO와 JDBC 사이에 위치하여 서로를 매핑해줍니다.

[그림 1] MyBatis의 위치

MyBatis-Spring은 MyBatis를 Spring Framework에 녹여 더 쉽게 사용할 수 있게 하기 위한 연동 모듈입니다. 

MyBatis-Spring은 밑에서 다루도록 하고, MyBatis를 계속 살펴보겠습니다.

 

MyBatis의 주요 Component와 동작 과정을 그림으로 표현하면 다음과 같습니다.

[그림 2] MyBatis의 Component와 동작 과정

  1. MyBatis Config 파일을 읽어 SqlSessionFactoryBuilder 객체를 생성합니다.
    MyBatis Config 파일에는 DB설정 정보, mapper 파일 등록, typeAlias설정 등이 들어있습니다.
  2. SqlSessionFactoryBuilder 객체를 이용해 SqlSessionFactory 객체를 생성합니다.
    SqlSessionFactoryBuilder는 단순히 SqlSessionFactory 객체를 생성해주기 위한 용도입니다.
  3. 앱 실행 중(런타임)에 CRUD처리가 들어오면 SqlSessionFactory로 SqlSession 객체를 생성합니다.
  4. SqlSession 객체를 이용해 DB요청을 한 후, 결과값을 받아옵니다.

하나씩 코드로 살펴보겠습니다.

먼저 MyBatis Config 파일은 XML로 작성하고, 다음과 같이 작성할 수 있습니다.

아래 MyBatis Config XML 파일은 4개의 정보를 설정하고 있습니다.

1. <properties>: 프로퍼티 파일을 정의합니다.

2. <typeAliases>: mapper.xml에서 사용할 alias를 설정합니다. (긴 패키지 경로 대신 짧은 단어 하나로 사용하기 위해)

3. <environments>: DB 정보를 설정합니다. <property>에 ${driver}로 적은 것은 위 <properties>에서 가져온 내용을 대입한다는 의미입니다.

4. <mappers>: 어떤 XML 파일이 mapper 파일인지를 설정합니다.

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" 
	"http://mybatis.org/dtd/mybatis-3-config.dtd">
	
<configuration>
    <properties resource="dbinfo.properties"/>
    <typeAliases>
        <typeAlias type="com.pangtrue.model.MemberDto" alias="member" />
    </typeAliases>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${driver}"/>
                <property name="url" value="${url}"/>
                <property name="username" value="${dbid}"/>
                <property name="password" value="${dbpwd}"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="member.xml" />
    </mappers>
</configuration>

다음으로 위 <mapper>에서 정의한 member.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.pangtrue.model.dao.UserDao">
    <select id="login" parameterType="map" resultType="member">
        SELECT username, userid, email
        FROM ssafy_member
        WHERE userid = #{userid}
        AND userpwd = #{userpwd}
    </select>
</mapper>

다음은 SqlSessionFactoryBuilder와 SqlSessionFactory를 생성하는 코드입니다.

위의 MyBatis 설정 파일을 읽어 SqlSessionFactoryBuilder를 생성한 후, 이걸로 SqlSessionFactory를 생성해주고 있습니다.

public class SqlMapConfig {
    private static SqlSessionFactory factory;
    static {
        try {
            String resource = "mybatis-config.xml";
            Reader reader = Resources.getResourceAsReader(resource);
            factory = new SqlSessionFactoryBuilder().build(reader);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static SqlSession getSqlSession() {
        return factory.openSession();
    }
}

마지막으로 이것을 런타임에 사용하는 코드는 다음과 같이 작성할 수 있습니다.

보시면 런타임에 SqlSession 객체를 생성한 후, SqlSession 메서드의 파라미터로 mapper idmapper 파라미터를 넘깁니다.

(위 mapper.xml 파일에 보시면 <select id=".." > 처럼 되어있죠? 해당 id를 아래에서 사용하는 겁니다. 🙂 )

@Repository
public class MemberDaoImpl implements MemberDao {
    private static final String NAMESPACE = "com.pangtrue.model.dao.MemberDao."; 
    @Override
    public void login(MemberDto memberDto) throws SQLException {
        try (SqlSession session = SqlMapConfig.getSqlSession()) {
            session.insert(NAMESPACE + "login", MemberDto);
            session.commit();
        }
    }
}

2. MyBatis-Spring란?


MyBatis-Spring은 MyBatis를 Spring Framework에 녹여내 좀 더 쉽게 사용하고자하는 연동 모듈입니다.

 

SqlSessionFactoryBean과 SqlSession을 Spring Framework의 Bean으로 등록합니다. 
SqlSession을 빈으로 등록할 때 class 속성을 보시면 SqlSessionTemplate라고 되어있습니다. 이는 SqlSession의 구현체입니다.

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="net.sf.log4jdbc.sql.jdbcapi.DriverSpy" />
    <property name="url" value="jdbc:log4jdbc:mysql://127.0.0.1:3306/pangtrue?useSSL=false" />
    <property name="username" value="pangtrue" />
    <property name="password" value="pangtrue1234!@#$" />
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="configLocation" value="classpath:/mybatis-config.xml"/>
    <property name="mapperLocations" value="classpath:mappers/user/userMapper.xml"/>
</bean>
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate" destroy-method="clearCache">
    <constructor-arg name="sqlSessionFactory" ref="sqlSessionFactory"/>
</bean>

<!-- MyBatis-Spring을 사용하는 이유가 아래의 스캔기능때문이라 봐도 무방합니다. -->
<!-- 해당 패키지 하위의 클래스를 검색해 매퍼.xml과 비교해서 맞는 것끼리 sqlSession.getMapper(매퍼.class)를 빈으로 등록해줍니다. -->
<mybatis-spring:scan base-package="com.pangtrue.guestbook.model.mapper"></mybatis-spring:scan>

mapper 설정 파일은 아래와 같이 작성할 수 있습니다.

<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  
<mapper namespace="com.pangtrue.mappers.user.UserMapper">
    <resultMap id="userVOResultMap" type="UserVO">
        <id property="userId" column="user_id"/>
        <result property="userPw" column="user_pw"/>
        <result property="userName" column="user_name"/>
        <result property="userEmail" column="user_email"/>
        <result property="userJoinDate" column="user_join_date"/>
        <result property="userLoginDate" column="user_login_date"/>
    </resultMap>
    
    <select id="selectUserById" parameterType="string" resultMap="UserVOResultMap">
        SELECT * FROM user WHERE user_id = #{userId}
    </select>
    
    <insert id="insertUser" parameterType="UserVO">
        INSERT INTO user VALUES(#{userId}, #{userPw}, #{userName}, #{userEmail})
    </insert>
</mapper>

MyBatis 설정 파일(mybatis-config.xml)에선 간단히 alias만 작성하겠습니다.

<!DOCTYPE configuration   
        PUBLIC "-//mybatis.org/DTD Config 3.0/EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">  <!-- XML 문서의 유효성 체크를 위해 필요 -->
        
<configuration>
    <typeAliases>
        <typeAlias alias="UserVO" type="com.pangtrue.user.domain.UserVO" />
    </typeAliases>
</configuration>

이제 모든 사전 준비는 끝났고, DAO를 이용해 다음처럼 사용합니다.

보시면 UserDao는 class가 아니라 interface입니다. 구현 객체를 굳이 만들어주지 않아도 되는 이유는 위 Spring 빈으로 <mybatis-spring:scan> 태그가 해당 interface와 매핑되는 mapper.xml을 알아서 찾아주기 때문입니다.

즉, 개발자가 SqlSession 객체를 생성하고 파라미터로 매핑되는 mapper id를 넘겨주지 않아도 mybatis-spring이 알아서 해줍니다. 

public interface UserDao {
    public MemberDto login(Map<String, String> map) throws SQLException;
}

3. 1:N 관계 매핑하기


아래 내용은 MyBatis 사용법을 개인적으로 기록해두고싶어 작성한 글입니다. 

다음과 같은 1:N 관계의 테이블이 있다고 해보겠습니다.

[그림 1] 1:N 테이블

model 객체는 다음과 같습니다.

public class Order {
    private Integer id;
    private String userId;
    private String orderTable;
    private Date orderTime;

    private List<OrderDetail> details;
}

MyBatis를 이용해 위 details 리스트를 채우려면 어떻게 해야할까요?

resultMap과 collection을 사용해야하는데요, order.xml(매퍼)는 다음과 같습니다.

<mapper namespace="com.demo.model.dao.OrderDao">
  <select id="selectWithDetail" parameterType="int" resultMap="withDetailMap">
    SELECT o.o_id, o.user_id, o.order_table, o.order_time, o.completed
    FROM t_order o
    WHERE o.o_id = #{id}
  </select>
  <resultMap id="withDetailMap" type="Order">
    <id column="o_id" property="id" />
    <result column="user_id" property="userId" />
    <result column="order_table" property="orderTable" />
    <result column="order_time" property="orderTime" />
    <collection property="details" column="o_id" javaType="java.util.ArrayList" ofType="OrderDetail" select="com.demo.model.dao.OrderDetailDao.selectByOrderId" />
  </resultMap>
</mapper>

다음으로 orderDetail.xml(매퍼)는 다음과 같습니다.

<mapper namespace="com.demo.model.dao.OrderDetailDao">
  <select id="selectByOrderId" parameterType="int" resultType="OrderDetail">
    SELECT d_id AS "id", order_id, product_id, quantity
    FROM t_order_detail
    WHERE order_id = #{detailId}
  </select>
</mapper>