🌿Interface Mapper
1. XML Mapper를 사용하는 방식
2. Interface Mapper를 사용하는 방식
이전 MyBatis 글에서 MyBatis를 사용하는 방식은 위 두 가지가 있다고 했다.
이번에는 Interface Mapper 방식으로 사용해 보도록 하자.
🌿프로젝트 설정
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.test</groupId>
<artifactId>simple</artifactId>
<name>MybatisSimpleTest</name>
<packaging>war</packaging>
<version>1.0.0-BUILD-SNAPSHOT</version>
<properties>
<java-version>11</java-version>
<org.springframework-version>5.0.7.RELEASE</org.springframework-version>
<org.aspectj-version>1.6.10</org.aspectj-version>
<org.slf4j-version>1.6.6</org.slf4j-version>
</properties>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${org.springframework-version}</version>
<exclusions>
<!-- Exclude Commons Logging in favor of SLF4j -->
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<!-- AspectJ -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${org.aspectj-version}</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${org.slf4j-version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>${org.slf4j-version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${org.slf4j-version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!-- @Inject -->
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
<!-- Servlet / JSP -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.3</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- Test -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<!-- <scope>test</scope> -->
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<!-- AOP -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${org.aspectj-version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${org.aspectj-version}</version>
</dependency>
<!-- JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.15.0</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<!-- HikariCP -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>2.7.4</version>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<!-- log4jdbc.log4j2 -->
<dependency>
<groupId>org.bgee.log4jdbc-log4j2</groupId>
<artifactId>log4jdbc-log4j2-jdbc4</artifactId>
<version>1.16</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-eclipse-plugin</artifactId>
<version>2.9</version>
<configuration>
<additionalProjectnatures>
<projectnature>org.springframework.ide.eclipse.core.springnature</projectnature>
</additionalProjectnatures>
<additionalBuildcommands>
<buildcommand>org.springframework.ide.eclipse.core.springbuilder</buildcommand>
</additionalBuildcommands>
<downloadSources>true</downloadSources>
<downloadJavadocs>true</downloadJavadocs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.5.1</version>
<configuration>
<source>11</source>
<target>11</target>
<compilerArgument>-Xlint:all</compilerArgument>
<showWarnings>true</showWarnings>
<showDeprecation>true</showDeprecation>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.2.1</version>
<configuration>
<mainClass>org.test.int1.Main</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root-context.xml</param-value>
</context-param>
<!-- Creates the Spring Container shared by all Servlets and Filters -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Processes application requests -->
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<servlet-name>appServlet</servlet-name>
</filter-mapping>
</web-app>
log4jdbc.log4j2.properties
log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
log4j(log level)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration PUBLIC "-//APACHE//DTD LOG4J 1.2//EN" "log4j.dtd">
<log4j:configuration
xmlns:log4j="http://jakarta.apache.org/log4j/">
<!-- Appenders -->
<appender name="console"
class="org.apache.log4j.ConsoleAppender">
<param name="Target" value="System.out" />
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%-5p: %c - %m%n" />
</layout>
</appender>
<!-- Application Loggers -->
<logger name="com.test.simple">
<level value="info" />
</logger>
<!-- 3rdparty Loggers -->
<logger name="org.springframework.core">
<level value="info" />
</logger>
<logger name="org.springframework.beans">
<level value="info" />
</logger>
<logger name="org.springframework.context">
<level value="info" />
</logger>
<logger name="org.springframework.web">
<level value="info" />
</logger>
<!-- Root Logger -->
<root>
<priority value="warn" />
<appender-ref ref="console" />
</root>
<logger name="jdbc.sqlonly">
<level value="info" />
</logger>
<logger name="jdbc.resultsettable">
<level value="info" />
</logger>
<logger name="jdbc.audit">
<level value="warn" />
</logger>
<logger name="jdbc.resultset">
<level value="warn" />
</logger>
<logger name="jdbc.connection">
<level value="warn" />
</logger>
<logger name="jdbc.sqltiming">
<level value="off" />
</logger>
</log4j:configuration>
servlet-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
<!-- Enables the Spring MVC @Controller programming model -->
<annotation-driven />
<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
<resources mapping="/resources/**" location="/resources/" />
<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<beans:property name="prefix" value="/WEB-INF/views/" />
<beans:property name="suffix" value=".jsp" />
</beans:bean>
<context:component-scan base-package="com.test.simple" />
<context:component-scan base-package="com.test.controller" />
</beans:beans>
🍃root-context.xml
Interface Mapper를 쓰려면 root-context.xml을 수정해야 한다.
mapperLocations과 sessionfactory가 필요가 없다.
<!-- 인터페이스 메퍼를 인식하는 행동 -->
<mybatis-spring:scan base-package="com.test.mapper"/>
Namespaces에서 mybatis-spring을 추가하고 위 코드를 작성해 주었다.
<?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:mybatis-spring="http://mybatis.org/schema/mybatis-spring"
xsi:schemaLocation="http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring-1.2.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- Root Context: defines shared resources visible to all other web components -->
<bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
<property name="driverClassName"
value="net.sf.log4jdbc.sql.jdbcapi.DriverSpy"></property>
<property name="jdbcUrl"
value="jdbc:log4jdbc:oracle:thin:@localhost:1521:xe"></property>
<property name="username" value="hr"></property>
<property name="password" value="java1234"></property>
</bean>
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"
destroy-method="close">
<constructor-arg ref="hikariConfig"></constructor-arg>
</bean>
<bean id="sessionfactory"
class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"></property>
<!--
<property name="mapperLocations"
value="classpath*:mapper/*.xml"></property>
-->
</bean>
<!--
<bean class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg ref="sessionfactory"></constructor-arg>
</bean>
-->
<!-- 인터페이스 메퍼를 인식하는 행동 -->
<mybatis-spring:scan base-package="com.test.mapper"/>
</beans>
전체적으로 root-context.xml은 이런 코드가 된다.
이제 Mapper 인터페이스를 만들어야 한다.
🌿Interface Mapper 생성
src/main/java > com.test.mapper
- TestMapper.java (I)
SqlSesstionTemplate(이전 사용) > TestMapper(대신 사용)
지금 만든 인터페이스가 쿼리를 날리는 SQL을 실행하는 담당자 역할을 한다.
인터페이스가 대신 쿼리를 날린다고 해서 Interface Mapper라고 하는 것이다.
Interface Mapper의 사용
package com.test.mapper;
import org.apache.ibatis.annotations.Select;
//인터페이스 매퍼
public interface TestMapper {
//어노테이션(SQL 구현)
@Select("select count(*) from tblAddress")
String getTime();
String getTime2();
}
인터페이스의 추상 메서드 위에 @Select, @Insert, @Update, @Delete 등의 쿼리를 작성할 때 사용하던 어노테이션을 종류별로 사용할 수 있다.
이 어노테이션 안에 쿼리를 작성할 수 있다. 즉, xml 파일이 없어도 쿼리를 작성할 수 있기 때문에 xml의 위치를 알려주지 않은 것이다.
🍃MapperTest.java
package com.test.persistence;
import static org.junit.Assert.assertNotNull;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.test.mapper.TestMapper;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml")
public class MapperTest {
@Autowired
private TestMapper mapper;
@Test
public void testMapper() {
assertNotNull(mapper);
System.out.println(mapper.getTime());
System.out.println(mapper.getTime2());
}
}
자료형이 인터페이스이기 때문에 TestMapper로 객체를 만든 게 아니라 TestMapper를 상속받는 클래스의 객체를 만든 것이다.
그런데 이를 부모로 하는 자식 객체의 구현체를 만든 적이 없다. 그럼에도 불구하고 Interface Mapper는 Spring이 알아서 이를 구현하는 클래스를 내부적으로 만든다.
그래서 우리는 Interface를 만드는 것만 하고 자식 클래스를 따로 만들지 않는다.
어노테이션 방식의 장단점
- 장점: SQL 쿼리를 날리기 편리하다.
- 단점: SQL 쿼리를 작성하기 불편하다.
기존의 Mapper 방식은 쿼리가 복잡하든 말든 xml에서 자유롭게 편집을 할 수 있는데, Interface Mapper 방식은 오로지 어노테이션 안에 쿼리를 작성해야 하므로 편리함이 오히려 불편함이 될 수 있다. 그래서 Interface Mapper 방식도 쿼리를 xml에 작성하는 기능을 제공한다.
어노테이션 방식으로 쿼리를 xml에 작성
src/main/resources > com > test > mapper
- TestMapper.xml
한 번에 폴더를 만들면 오류가 나는 버그가 있기 때문에 폴더를 하나씩 만들어야 한다.
xml이 위치한 폴더는 반드시 Interface Mapper가 들어 있는 패키지의 구조와 동일해야 한다.
이렇게 하면 Interface Mapper가 XML의 위치를 자동으로 인식하게 된다.
TestMapper.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.test.mapper.TestMapper">
<select id="getTime2" resultType="String">
select sysdate from dual
</select>
</mapper>
mapper의 namespace에는 호출할 인터페이스를 적어야 한다. 그래야 서로 연동이 된다.
그리고 id를 적을 때 interface에 있던 추상 메서드의 이름을 적어준다. 그러면 Spring이 알아서 짝꿍으로 해당 아이디를 가지고 있는 쿼리를 연결해 준다.
이처럼 간단한 쿼리는 어노테이션에서 적지만, 긴 쿼리는 xml에 작성하면서 혼용하여 사용한다.
🌿Interface Mapper 주소록 구현
파일 생성
com.test.controller
- AddressController.java
com.test.domain
- AddressDTO.java
com.test.mapper
- AddressMapper.java(I)
src/main/resources > com > test > mapper
- AddressMapper.xml
views
- list.jsp
AddressMapper.java(I)가 DAO의 역할을 대신하기 때문에 DAO가 없다.
물론 DAO를 만들어도 되지만, 이번에는 DAO를 만들지 않고 구현해 보도록 하자.
연동이 잘 되는지 확인하기 위해 list.jsp를 만들었다.
AddressController.java
package com.test.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import com.test.mapper.AddressMapper;
@Controller
public class AddressController {
@Autowired
private AddressMapper mapper;
@GetMapping(value = "/list.do")
public String list(Model model) {
model.addAttribute("list", mapper.list());
model.addAttribute("dto", mapper.get());
return "list";
}
}
AddressMapper mapper가 SQL 쿼리를 날릴 수 있는 자격을 갖추기 위해서는 root-context.xml에서 인터페이스가 등록이 되어 있어야 한다.
mapper의 추상 메서드를 호출하면 ArrayList를 가져오도록 AddressMapper.xml에서 설정했으므로 이를 model에 넣어서 보내도록 한다.
AddressDTO.java
package com.test.domain;
import lombok.Data;
@Data
public class AddressDTO {
private String seq;
private String name;
private String age;
private String gender;
private String address;
private String regdate;
}
AddressMapper.java(I)
package com.test.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Select;
import com.test.domain.AddressDTO;
public interface AddressMapper {
@Select("select * from tblAddress")
List<AddressDTO> list();
AddressDTO get();
}
목록 보기를 하기로 했으므로 호출할 때 List로 받는다.
AddressMapper.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.test.mapper.AddressMapper">
<select id="get" resultType="com.test.domain.AddressDTO">
select * from tblAddress where seq = 1
</select>
</mapper>
xml 파일은 최소한의 기본 코드를 작성해야 오류가 발생하지 않는다.
list.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<link rel="stylesheet" href="https://me2.do/5BvBFJ57">
<style>
</style>
</head>
<body>
<!-- list.jsp -->
<table>
<c:forEach items="${list}" var="dto">
<tr>
<td>${dto.name}</td>
<td>${dto.age}</td>
<td>${dto.gender}</td>
<td>${dto.address}</td>
<td>${dto.regdate}</td>
</tr>
</c:forEach>
</table>
<hr>
<div>${dto}</div>
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script>
</script>
</body>
</html>