잊지 않겠습니다.

15. Spring과 iBatis

Java 2013. 9. 11. 14:18

* 사내 강의용으로 사용한 자료를 Blog에 공유합니다. Spring을 이용한 Web 개발에 대한 전반적인 내용에 대해서 다루고 있습니다.



Spring에서 myBatis를 사용하기 위해서는 Hibernate는 Spring이 Hibernate에 대한 SessionFactory, DataSource를 제공하고 있는 것에 비해, 오히려 mybatis 측에서 Spring에 대한 library를 제공하고 있습니다. 

pom.xml에 다음과 같은 내용을 추가해주시길 바랍니다. 

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>1.1.1</version>
</dependency>

pom.xml에 적용후, command창에서 mvn dependency:tree 를 한번 실행시켜보시면 다음과 같은 결과를 볼 수 있습니다. 




기본적으로 mybatis-spring은 spring 3.1RELEASE를 가지고 개발된 jar입니다. 따라서, mybatis-spring을 참조하는 즉시, spring 3.1Release가 바로 추가가 됩니다. 그런데, 저희 프로젝트는 기본적으로 Spring 3.2RELEASE를 기반으로 구성되어 있습니다. 이런 경우 어떻게 해야지 될까요?
다행히도, Spring은 하위 호환성을 100% 맞추고 있습니다. 따라서, mybatis-spring에서 사용하고 있는 Spring 3.1 module을 모두 제거하고, Spring 3.2 RELEASE를 모두 사용하도록 maven의 dependency를 조절해줘야지 됩니다. maven에서 dependency 된 jar안에 포함된 다른 module을 제거하는 것은 다음과 같은 포멧으로 pom.xml을 수정해줘야지 됩니다.

이러한 경우에 pom.xml을 수정하는 방법은 2가지가 있습니다. 

1. 각 dependency에서 각 library들을 exclude 시켜주는 방법
2. dependencyManagement에서 각 library들의 version을 관리해주는 방법

개인적으로는 2번째의 방법이 좀 더 나은 것 같습니다. dependencyManagement를 이용해서 mybatis-spring의 dependency 충돌을 해결한 pom.xml의 일부입니다. 

   <repositories>
        <repository>
            <id>mybatis-snapshot</id>
            <name>MyBatis Snapshot Repository</name>
            <url>https://oss.sonatype.org/content/repositories/snapshots</url>
        </repository>
    </repositories>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-test</artifactId>
                <version>3.2.1.RELEASE</version>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-jdbc</artifactId>
                <version>3.2.1.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-orm</artifactId>
                <version>3.2.1.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-tx</artifactId>
                <version>3.2.1.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-context</artifactId>
                <version>3.2.1.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-core</artifactId>
                <version>3.2.1.RELEASE</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.1.1</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.22</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>1.1.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
        </dependency>
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib-nodep</artifactId>
            <version>2.2.2</version>
        </dependency>
    </dependencies>

이제 dependency를 다시한번 확인해보도록 하겠습니다. 

mybatis-spring에서 의존하고 있던 jar들은 모두 제거가 된 것을 알 수 있습니다. 그리고 Spring에 필요한 jar들이 모두 추가 됨을 볼 수 있습니다.

마지막으로 applicationContext.xml을 추가하도록 하겠습니다. myBatis는 기본적으로 spring에서 제공하는 DataSource를 그대로 이용할 수 있습니다. DataSource를 그대로 이용한다는 것은 TransactionManager를 이용한 @Transactional 역시 사용이 가능하다는 뜻입니다. 지금까지 해왔던 내용대로 설정을 할 수 있습니다. 다만 차이가 있다면 Spring을 사용하지 않고 myBatis를 이용할 때는 SqlSession을 SqlSessionFactory에서 직접 얻어오는 방식을 채택했습니다. 그렇지만, Spring을 이용하는 경우, mybatis-spring에서 제공하는 SqlSessionFactoryBean과 SqlSessionFactoryTemplage을 이용해서 작업을 한다는 차이 이외에는 테스트 코드 자체도 큰 차이가 없습니다.

다음은 applicationContext.xml 입니다. 

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:context="http://www.springframework.org/schema/context"
  xmlns:tx="http://www.springframework.org/schema/tx"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
  <context:property-placeholder location="classpath:spring.property" />
  <bean id="dataSource"
    class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="${jdbc.driver}" />
    <property name="url" value="${jdbc.url}" />
    <property name="username" value="${jdbc.username}" />
    <property name="password" value="${jdbc.password}" />
  </bean>
  <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="configLocation" value="classpath:mybatis.xml" />
  </bean>
  <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
    <constructor-arg ref="sqlSessionFactory" />
  </bean>
  <bean id="transactionManager"
    class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
  </bean>
  <tx:annotation-driven transaction-manager="transactionManager" />
</beans>

dataSource와 transactionManager는 JdbcTemplate을 사용할때와 완전히 동일합니다. 그리고 Hibernate에서 사용하던 LocalSessionFactoryBean 대신에 SqlSessionFactoryBean과 SqlSessionTemplate를 사용하는 차이만을 가지고 있습니다. 

이제 구성된 설정을 이용해서 각 Dao interface들을 얻어보도록 하겠습니다. 

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
@Transactional
public class BookDaoTest {
    @Autowired
    private SqlSessionTemplate sqlSessionTemplate;
    private BookDao bookDao;
    
    @Before
    public void setUp() {
        assertThat(sqlSessionTemplate, is(not(nullValue())));
        bookDao = sqlSessionTemplate.getMapper(BookDao.class);
        bookDao.deleteAll();
        assertThat(bookDao.countAll(), is(0));
    }
    
    @Test
    public void add() {
        List<Book> books = getBooks();
        for(Book book : books) {
            bookDao.add(book);
        }
    }
    
    @Test
    public void getAll() {
        List<Book> books = getBooks();
        for(Book book : books) {
            bookDao.add(book);
        }
        List<Book> allBooks = bookDao.getAll();
        for(Book book : allBooks) {
            System.out.println(book);
        }
    }

기존에는 각각의 session에서 commit를 실행시켜줬지만, 이제는 @Transactional을 이용한 세련된 Transaction을 하고 있는 것을 볼 수 있습니다. 사용법 역시 큰 차이가 있지 않으며 Spring과 interface를 조합한 사용 계층에서의 큰 변경 없이 코드의 기술을 완전히 변경을 시킬 수 있는 것을 알 수 있습니다. 
코딩하는 연습을 하는 시간을 갖겠습니다. 나머지 코드들을 모두 작성해주세요. 


매우 간편한 설정 및 사용의 편의성. mybatis가 현업에서 가장 많이 사용되고 있는 이유가 보이시나요? 이렇게 쉬울수는 없다. 라는 느낌이 맞을 정도입니다. 


Summary

지금까지 구성된 bookstore를 myBatis-spring을 이용해서 구성해주세요. 



Posted by Y2K
,

Spring 4.0 에서 제공되는 핵심 기능 변경사항중 가장 주목할 점이 있다면 WebSocket의 지원입니다.


일단 Java에서는 Java EE 7에서 WebSocket에 대한 기본 API는 모두 구성이 마쳐져 있습니다. 당장 WebSocket을 지원하는 Web Page를 구성하기 위해서는 다음과 같은 조건들이 필요합니다.


1. Java EE 7 이상 지원

2. 최신 WAS 지원 (tomcat 8 이상, jetty 9.0.4 이상)


Spring에서는 WebSocket을 다음과 같은 방법으로 지원하고 있습니다.


1. @ServerEndPoint를 이용한 Java 기본 API

2. WebSocketHandler를 이용한 구성 - Spring WebSocket API


Spring WebSocket API의 경우, Java 기본 API와는 구성이 다릅니다. 이와 같은 구성을 갖게 되는 이유는 SocketJS와 같은 WebSocket을 이용하는 다른 API들을 사용할 수 있도록 한번 Wrapping을 거친 구성을 가지게 하는 것이 목표이기 때문입니다.. Spring WebSocket API의 경우, WebSocketHandler가 가장 핵심이 되고, 이를 이용한 코드 구성에 대해서 알아보도록 하겠습니다. 


먼저, pom.xml에 필요한 dependency를 설정합니다.

         <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-websocket</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring.version}</version>
        </dependency>


websocket의 경우, servlet 3.0을 사용해야지 되고, websocket에 대한 dependency는 다음과 같습니다.


        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.websocket</groupId>
            <artifactId>javax.websocket-api</artifactId>
            <version>1.0</version>
        </dependency>


간단한 EchoService를 지원하는 WebSocket 지원 WebPage를 구성합니다. 그에 따른 WebSocketHandler는 TextWebSocketHandlerAdapter를 이용합니다. 여담이지만, Spring에서 Adapter라는 접미사가 붙은 객체들은 대부분 상속을 통해서 좀더 편하게 설정들을 할 수 있도록 도와주는 Spring에서 제공하는 일종의 Helper Class 또는 Parent Class 들이라고 할 수 있습니다.


WebSocketHandlerAdapter에서는 다음 3개의 method를 주목할 필요가 있습니다. 


1. afterConnectionEstablished(WebSocketSession session)

: WebSocket connection이 발생되었을 때, 호출되는 method입니다. connection open이 된 후기 때문에, 해줘야지 될 일들을 처리하면 됩니다. 


2. afterConnectionClosed(WebSocketSession session, CloseStatus status)

: WebSocket connection이 끊겼을 때, 호출되는 method입니다. connection close가 된 후, 일을 처리하면 됩니다. 


3. handleMessage(WebSocketSession session, WebSocketMessage<?> message)

: 핵심적인 method입니다. 실질적인 통신 method입니다. socket.accept() 후에 onMessage 에서 처리될 일들을 이곳에서 coding하면 된다고 생각하면 됩니다. 


한번 코드를 확인해보도록 하겠습니다. 


public class EchoWebSocketHandler extends TextWebSocketHandlerAdapter {

    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
        String payloadMessage = (String) message.getPayload();
        session.sendMessage(new TextMessage("ECHO : " + payloadMessage));
    }

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // Connection이 구성된 후, 호출되는 method
        super.afterConnectionEstablished(session);
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        // Connection이 종료된 후, 호출되는 method
        super.afterConnectionClosed(session, status);
    }
}


WebSocket을 지원하는 Handler의 구성이 모두 마쳐진 후, WebSocketHandler를 Spring @MVC에 통합하기 위해서는 Config를 다음과 같이 구성합니다. 


@Configuration
@EnableWebMvc
@EnableWebSocket
public class WebConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // WebSocket을 /echo 에 연결합니다.
        registry.addHandler(echoHandler(), "/echo");

        // SocketJS 지원 url을 /socketjs/echo에 연결합니다.
        registry.addHandler(echoHandler(), "/socketjs/echo").withSockJS();
    }

    @Bean
    public WebSocketHandler echoHandler() {
        return new EchoWebSocketHandler();
    }

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
}

매우 단순한 구조로 Config를 구성할 수 있습니다. Config에서 주목할 것은 WebSocketConfigurer interface입니다. 이 interface를 통해서 WebSocket Handler를 아주 쉽게 구성할 수 있습니다. 


마지막으로 web.xml을 대신할 DispatcherWebApplicationIntializer 입니다. 특별한 것은 없고, Servlet Configuration에서 만들어진 WebConfig.class를 반환합니다. 


public class DispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return null;
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { WebConfig.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }

    @Override
    protected void customizeRegistration(Dynamic registration) {
        registration.setInitParameter("dispatchOptionsRequest", "true");
    }

}


이제 WebSocket을 테스트 하기 위해 간단한 HTML Page를 구성해보도록 하겠습니다.


echo.html은 다음과 같이 구성할 수 있습니다. 


<!DOCTYPE html>
<html>
<head>
    <title>WebSocket/SockJS Echo Sample (Adapted from Tomcat's echo sample)</title>
    <style type="text/css">
        #connect-container {
            float: left;
            width: 400px
        }

        #connect-container div {
            padding: 5px;
        }

        #console-container {
            float: left;
            margin-left: 15px;
            width: 400px;
        }

        #console {
            border: 1px solid #CCCCCC;
            border-right-color: #999999;
            border-bottom-color: #999999;
            height: 170px;
            overflow-y: scroll;
            padding: 5px;
            width: 100%;
        }

        #console p {
            padding: 0;
            margin: 0;
        }
    </style>

    <script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>

    <script type="text/javascript">
        var ws = null;
        var url = null;
        var transports = [];

        function setConnected(connected) {
            document.getElementById('connect').disabled = connected;
            document.getElementById('disconnect').disabled = !connected;
            document.getElementById('echo').disabled = !connected;
        }

        function connect() {
            if (!url) {
                alert('Select whether to use W3C WebSocket or SockJS');
                return;
            }

            ws = (url.indexOf('socketjs') != -1) ? 
                new SockJS(url, undefined, {protocols_whitelist: transports}) : new WebSocket(url);

            ws.onopen = function () {
                setConnected(true);
                log('Info: connection opened.');
            };
            ws.onmessage = function (event) {
                log('Received: ' + event.data);
            };
            ws.onclose = function (event) {
                setConnected(false);
                log('Info: connection closed.');
                log(event);
            };
        }

        function disconnect() {
            if (ws != null) {
                ws.close();
                ws = null;
            }
            setConnected(false);
        }

        function echo() {
            if (ws != null) {
                var message = document.getElementById('message').value;
                log('Sent: ' + message);
                ws.send(message);
            } else {
                alert('connection not established, please connect.');
            }
        }

        function updateUrl(urlPath) {
            if (urlPath.indexOf('socketjs') != -1) {
                url = urlPath;
                document.getElementById('sockJsTransportSelect').style.visibility = 'visible';
            }
            else {
              if (window.location.protocol == 'http:') {
                  url = 'ws://' + window.location.host + urlPath;
              } else {
                  url = 'wss://' + window.location.host + urlPath;
              }
              document.getElementById('sockJsTransportSelect').style.visibility = 'hidden';
            }
        }

        function updateTransport(transport) {
          transports = (transport == 'all') ?  [] : [transport];
        }
        
        function log(message) {
            var console = document.getElementById('console');
            var p = document.createElement('p');
            p.style.wordWrap = 'break-word';
            p.appendChild(document.createTextNode(message));
            console.appendChild(p);
            while (console.childNodes.length > 25) {
                console.removeChild(console.firstChild);
            }
            console.scrollTop = console.scrollHeight;
        }
        function clear() {
            $('#message').html('');
        }
    </script>
</head>
<body>
<div>
    <div id="connect-container">
        <input id="radio1" type="radio" name="group1" onclick="updateUrl('/tutorial01/echo');">
            <label for="radio1">W3C WebSocket</label>
        <br>
        <input id="radio2" type="radio" name="group1" onclick="updateUrl('/tutorial01/socketjs/echo');">
            <label for="radio2">SockJS</label>
        <div id="sockJsTransportSelect" style="visibility:hidden;">
            <span>SockJS transport:</span>
            <select onchange="updateTransport(this.value)">
              <option value="all">all</option>
              <option value="websocket">websocket</option>
              <option value="xhr-polling">xhr-polling</option>
              <option value="jsonp-polling">jsonp-polling</option>
              <option value="xhr-streaming">xhr-streaming</option>
              <option value="iframe-eventsource">iframe-eventsource</option>
              <option value="iframe-htmlfile">iframe-htmlfile</option>
            </select>
        </div>
        <div>
            <button id="connect" onclick="connect();">Connect</button>
            <button id="disconnect" disabled="disabled" onclick="disconnect();">Disconnect</button>
        </div>
        <div>
            <textarea id="message" style="width: 350px">Here is a message!</textarea>
        </div>
        <div>
            <button id="echo" onclick="echo();" disabled="disabled">Echo message</button>
        </div>
    </div>
    <div id="console-container">
        <div id="console"></div>
    </div>
</div>
</body>
</html>


이제 구성된 Page의 테스트를 위해 maven jetty를 build 항목에 추가합니다. 


    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.3.2</version>
                <configuration>
                    <source>1.7</source>
                    <target>1.7</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>2.2</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <version>2.5</version>
                <configuration>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-eclipse-plugin</artifactId>
                <version>2.8</version>
                <configuration>
                    <downloadSources>true</downloadSources>
                    <downloadJavadocs>true</downloadJavadocs>
                    <wtpversion>2.0</wtpversion>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.eclipse.jetty</groupId>
                <artifactId>jetty-maven-plugin</artifactId>
                <version>9.0.4.v20130625</version>
                <configuration>
                    <webApp>
                        <contextPath>/${project.artifactId}</contextPath>
                    </webApp>
                </configuration>
            </plugin>
        </plugins>
    </build>


구성후, mvn jetty:run 명령어를 통해서 실행이 가능합니다. 


websocket-tutorial01.zip





Posted by Y2K
,

14. iBatis (myBatis)

Java 2013. 9. 10. 10:27

* 사내 강의용으로 사용한 자료를 Blog에 공유합니다. Spring을 이용한 Web 개발에 대한 전반적인 내용에 대해서 다루고 있습니다.



ibatis (mybatis)

지금까지 직접 JDBC를 이용한 DB접속 방법과 Hibernate를 이용한 DB 접속 방법을 알아봤습니다.
DB라는 관계형 데이터를 java라는 객체 지향의 언어에 mapping을 시키는 과정은 참 여러가지 기술적인 요인들과 방법들을 계속해서 내보이고 있습니다.
그 중에서 SQL을 직접 사용하는 가장 low level의 JDBC, 그리고 SQL을 최대한 사용하지 않고 객체만으로 표현하는 Hibernate는 그 대표적인 기술이라고 할 수 있습니다. 그런데, 국내에서는 다른 기술을 더 많이 쓰고 있는 것이 사실입니다. 

iBatis(아래 부터는 myBatis)가 바로 그 기술입니다. 다른 것보다는 이 기술은 직접 sql을 사용한다는 큰 차이를 가지고 있습니다. 그렇지만, 기존에 우리가 만들었던 것과 같은 entity-dao-service layer를 충실히 지킬 수 있으며 기존의 SP와 같은 legacy sql query를 직접적으로 사용하고, code로서 관리한다는 점이 가장 큰 차이입니다.

iBatis는 JDBC를 이용한 반복적인 코드를 획기적으로 줄이는 것을 목표로 가지고 있으며, 개발자들에게 게을러질 수 있는 권리를 보장하고 있습니다. 

ibatis의 구조

Hibernate와 비슷하게 SqlSessionFactory, SqlSession이라는 두개의 객체를 가지고 있습니다. 기본적으로 mybatis.cfg.xml 파일을 이용해서 DB에 대한 연결 설정과 각각의 mapper를 구성하는 설정으로 구성되어 있습니다.

ibatis에서 DB에 대한 연결을 구성하는 방법은 다음과 같습니다. 

1. SqlSessionFactory 구성 
2. SqlSessionFactory를 통한 SqlSession을 구성
3. SqlSession을 통해 mapper를 구성

여기서 새로운 개념들이 조금 나오게 되는데요. Hibernate에서 많은 개념들을 차용해온것들이 나오게 됩니다. 

먼저, SqlSessionFactory는 이름 그대로 SessionFactory입니다. 

일단 SqlSession을 통해서 얻어지는 mapper는 기본적으로 dao 객체들입니다.

 
백문이 불여 일타. 한번 지금까지 만들어진 코드를 구성해보도록 합시다. 

기존 jdbcTemplate에서 사용한 Book, User, History 객체와 Dao에 대한 interface를 모두 카피해와서 새로운 프로젝트를 구성합니다.
myBatis는 maven central repository에 없습니다. 다음 repository 설정을 추가하고, dependency를 추가하도록 합니다. 이와 같이 maven central repository에서 지원하지 않는 library들은 자신만의 repository를 갖는 경우가 많습니다. 그리고 사내에서는 nexus라는 maven server를 설치해서, 사내 repository를 구성해서 사용하는 경우도 많습니다.


  <repositories>
    <repository>
      <id>mybatis-snapshot</id>
      <name>MyBatis Snapshot Repository</name>
      <url>https://oss.sonatype.org/content/repositories/snapshots</url>
    </repository>
  </repositories>

그 후, myBatis를 추가합니다.
   <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.1.1</version>
    </dependency>

   
myBatis에서 DB에 접근하기 위한 순서인 SqlSessionFactory를 먼저 구성해보도록 하겠습니다. 
SqlSessionFactory는 xml 파일로 설정하게 되며, 다음과 같이 설정할 수 있습니다. 

<?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>
  <environments default="bookstore">
    <environment id="bookstore">
      <transactionManager type="JDBC" />
      <dataSource type="POOLED">
        <property name="driver" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost/bookstore" />
        <property name="username" value="root" />
        <property name="password" value="qwer12#$"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="com/xyzlast/mybatis/bookstore/dao/mappers/userDao.mapper.xml" />
    <mapper resource="com/xyzlast/mybatis/bookstore/dao/mappers/bookDao.mapper.xml" />
    <mapper resource="com/xyzlast/mybatis/bookstore/dao/mappers/historyDao.mapper.xml"/>
  </mappers>
</configuration>

먼저 myBatis는 한개의 xml에 여러개의 connection 정보들을 담을 수 있습니다. 각각의 환경에 대한 id를 설정하고, 그 환경에 대한 기본 설정을 해주게 됩니다. DataSource에서 자주 보던 driver, url, username, password에 대한 설정을 하게 되는 것을 알 수 있습니다. 그리고 mapper들을 설정해주게 됩니다. 이 mapper들은 xml로 구성되어 있으며, 1 개의 method에 1:1로 mapping되는 SQL query가 들어가 있습니다. 일반적으로 interface 이름 + mapper.xml 의 명명 규칙을 따르게 됩니다.

다음은 bookDao.mapper.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.xyzlast.mybatis.bookstore.dao.BookDao">
  <resultMap type="com.xyzlast.mybatis.bookstore.entities.Book" id="BookResult">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <result property="author" column="author"/>
    <result property="publishDate" column="publishDate"/>
    <result property="rentUserId" column="rentUserId"/>
    <result property="status" column="status" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
  </resultMap>

  <select id="get" parameterType="int" resultMap="BookResult">
    SELECT * FROM books WHERE id = #{bookId}
  </select>
  <select id="getAll" resultMap="BookResult">
    SELECT * FROM books
  </select>
  <select id="countAll" resultType="int">
    SELECT count(*) FROM books
  </select>
  <insert id="add" parameterType="com.xyzlast.mybatis.bookstore.entities.Book" useGeneratedKeys="true" keyColumn="id">
    INSERT INTO books(name, author, publishDate, comment, status, rentUserId)
    VALUES(#{name}, #{author}, #{publishDate}, #{comment}, #{status.value}, #{rentUserId})
  </insert>
  <delete id="delete" parameterType="int">
    DELETE FROM books WHERE id = #{bookId}
  </delete>
  <delete id="deleteAll">
    DELETE FROM books
  </delete>
  <update id="update" parameterType="com.xyzlast.mybatis.bookstore.entities.Book">
    UPDATE books SET
    name = #{name}, author = #{author}, publishDate=#{publishDate}, status=#{status.value}, rentUserId=#{rentUserId}
    WHERE id = #{id}
  </update>
  <select id="search" parameterType="String" resultMap="BookResult">
    SELECT * FROM books
    WHERE name like "%${value}%"
  </select>
</mapper>


먼저, 가장 주시해서 봐야지 되는 것은 mapper의 namespace입니다. namespace에는 interface에 대한 package명이 포함된 full name이 들어가야지 됩니다. 이 부분에 대한 설정이 잘못 되어 있으면 사용할 수 없습니다. 그 다음으로 봐야지 될 것은 resultMap입니다. return되는 query문의 결과가 어떤 DTO/VO 객체에 어떤 property에 mapping되는지에 대한 설정이 여기에 기록이 됩니다. 재미있는 것이 BookStatus enum값의 mapping입니다. org.apache.ibatis.type.EnumOrdinalTypehandler를 이용해서 enum값과 BookStatus를 mapping시킬 수 있습니다. 

BookDao interface와 mapping.xml 파일을 한번 비교해보도록 하겠습니다.




query를 각각 type에 맞추어 select/insert/delete/update로 나눠 등록을 하게 됩니다. 또한 query의 id는 각 method의 이름과 1:1로 mapping이 되게 됩니다. myBatis를 이용하는 경우에는 interface에 대한 객체를 따로 만들지 않기 때문에 코딩양이 줄어 들 수 있습니다. 또한, sql query를 project에서 관리하고 있기 때문에 query에 대한 관리 역시 용의한 장점을 가지고 있습니다. 

이제 SqlSessionFactory를 얻어내는 과정은 모두 완료되었습니다. SqlSessionFactory는 application에서 딱 1개만 존재하면 됩니다. 이제 이 SqlSessionFactory에서 SqlSession을 얻어오는 과정은 Hibernate에서 SessionFactory를 얻어내는 과정과 완전히 동일합니다. 다음은 BookDao의 테스트 코드의 일부입니다. 

public class BookDaoTest {
    private SqlSession session;
    private BookDao bookDao;
    private SqlSessionFactoryGenerator sqlSessionFactoryGenerator;

    @Before
    public void setUp() throws IOException {
        sqlSessionFactoryGenerator = new SqlSessionFactoryGenerator();
        sqlSessionFactoryGenerator.setXmlFilename("mybatis.xml");
        SqlSessionFactory factory = sqlSessionFactoryGenerator.getSqlSessionFactory();

        session = factory.openSession();
        bookDao = session.getMapper(BookDao.class);
        bookDao.deleteAll();
        session.commit();
        assertThat(bookDao.countAll(), is(0));
    }

    @After
    public void tearDown() {
        session.close();
    }

    @Test
    public void add() {
        int count = bookDao.countAll();
        List<Book> books = getBooks();
        for(Book book : books) {
            bookDao.add(book);
            session.commit();
            count++;
            assertThat(bookDao.countAll(), is(count));
        }
    }

SqlSessionFactory를 통해, SqlSession을 얻어내고 update/delete/insert에 대한 commit을 직접 행하도록 코드를 작성했습니다. 또한 모든 method가 완료되면 Hibernate와 동일하게, 반드시 Session을 닫아줘야지만 됩니다. 

Hibernate는 객체에 대한 xml 설정 또는 annotation 설정을 하는데 반하여, myBatis는 action에 설정을 하는 것을 주목해주세요. 이 둘의 차이는 매우 큰 차이를 가지고 오게 됩니다. 그리고, 지금 객체를 entities package에서 얻어오게 되었지만, 실질적으로 이 객체는 DTO또는 VO 객체가 됩니다. DB에서 값을 가지고 오는 역활만을 담당하는 객체로 보고 개발을 진행하는 것이 좋습니다. 


Summary

myBatis를 이용한 DAO 객체에 대해서 알아봤습니다. 지금까지 구성한 서비스까지 한번 구현해보세요. 이번에는 Spring을 사용하지 않고 구현하는 것이 목표입니다. 다음 장에서는 Spring을 이용해서 더욱더 간단하게 myBatis의 설정을 구축하는 것을 보여드리도록 하겠습니다. 





Posted by Y2K
,