[mybatis] mybatis 적용
https://yeo-computerclass.tistory.com/231
JSP & Servlet으로 구현한 프로젝트 관리에 mybatis를 적용해보도록 하겠다.
📌mybatis 흐름도
위 프로젝트 관리에서 mybatis를 이용하여 프로젝트 목록을 가져오는 흐름을 설명하도록 하겠다. 아래에 계속 설명을 보다가 이해를 돕기 위해서 mybatis 흐름도를 계속 보길 바란다.
- MySqlProjectDao는 SqlSessionFactory에게 SQL을 실행할 객체를 요구한다.
- SqlSessionFactory는 SqlSession 객체를 생성하여 반환한다.
- MySqlProjectDao는 SqlSession 객체에는 SQL 실행을 요청한다. (여기서는 List목록 요청)
- SqlSession 객체는 SQL이 저장된 맵퍼 파일에서 SQL을 찾는다.
- SqlSession은 JDBC 드라이버를 통해 DB에 질의를 실행한다.
- SqlSession은 DB로부터 가져온 데이터로 Project 목록을 생성하여 반환한다.
- MySqlProjectDao는 사용이 끝난 SqlSession을 닫는다.
📌mybatis 프레임워크의 핵심 컴포넌트
mybatis의 핵심 컴포넌트는 다음과 같다.
컴포넌트 | 설명 |
SqlSession | 실제 SQL을 실행하는 객체이다. 이 객체는 SQL을 처리하기 위해 JDBC 드라이버를 사용한다. |
SqlSessionFactory | SqlSession 객체를 생성한다. |
SqlSessionFactoryBuilder | mysql 설정 파일의 내용을 토대로 SqlSessionFactory를 생성한다. |
mybatis 설정 파일 | DB 연결 정보, 트랜잭션 정보, mybatis 제어 정보 등의 설정 내용을 포함하고 있다. SqlSessionFactory를 만들 때 사용된다. |
SQL 맴버 파일 | SQL문을 담고 있는 파일. SqlSession 객체가 참조한다. |
📌DAO에서 SqlSessionFactory 사용
※최상단 링크에서 MySqlProjectDao 클래스와 비교해서 보기
💡MySqlProjectDao
package spms.dao;
// mybatis 적용
import java.util.List;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import spms.annotation.Component;
import spms.vo.Project;
@Component("projectDao")
public class MySqlProjectDao implements ProjectDao {
SqlSessionFactory sqlSessionFactory;
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
}
public List<Project> selectList() throws Exception {
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
return sqlSession.selectList("spms.dao.ProjectDao.selectList");
} finally {
sqlSession.close();
}
}
public int insert(Project project) throws Exception {
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
int count = sqlSession.insert("spms.dao.ProjectDao.insert", project);
sqlSession.commit();
return count;
} finally {
sqlSession.close();
}
}
public Project selectOne(int no) throws Exception {
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
return sqlSession.selectOne("spms.dao.ProjectDao.selectOne", no);
} finally {
sqlSession.close();
}
}
public int update(Project project) throws Exception {
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
int count = sqlSession.update("spms.dao.ProjectDao.update", project);
sqlSession.commit();
return count;
} finally {
sqlSession.close();
}
}
public int delete(int no) throws Exception {
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
int count = sqlSession.delete("spms.dao.ProjectDao.delete", no);
sqlSession.commit();
return count;
} finally {
sqlSession.close();
}
}
}
의존 객체 SqlSessionFactory
이전에는 DB 커넥션을 얻기 위해 DataSource 객체가 필요했다. 그래서 DataSource에 대한 인스턴스 변수 선언과, setDataSource 메소드를 선언했었다. 하지만 mybatis를 사용하면 DataSource는 더 이상 필요없다.
대신 SqlSessionFactory 객체가 필요하다. SqlSessionFactory는 SQL을 실행할 때 사용할 도구를 만들어준다.
SqlSession의 주요 메소드
SqlSessionFactory는 SqlSession 객체를 생성하여 반환한다.(opensession() 메소드 사용)
SqlSession은 SQL문을 실행할 때 사용할 수 있는 메소드들을 갖추고 있다. 주요 메소드는 다음과 같다.
메소드 | 설명 |
selectList() | SELECT 문을 실행. 값 객체(VO) 목록을 반환한다. |
selectOne() | SELECT 문을 실행. 하나의 값 객체를 반환한다. |
insert() | INSERT 문을 실행. 반환값은 입력한 데이터 개수 |
update() | UPDATE 문을 실행. 반환값을 변경한 데이터 개수 |
delete() | DELETE 문을 실행. 반환값을 삭제한 데이터 개수 |
SqlSession의 호출 문법
selectList()나 selectOne() ... delete()를 호출할 때 sqlSession.XXX(매개변수)로 호출을 한다. 이 때 매개변수에 관해 설명을 하도록하겠다. 일단 첫 번째 매개변수 값은 SQL 아이디이다. SQL 아이디는 SQL 맵퍼의 네임스페이스 이름과 SQL 문의 아디이를 결합하여 만든 문자열이다.
SQL ID = SQL mapper namespace 이름 + SQL문 ID
아래 MySqlProjectDao.xml 즉 SQL 맵퍼 파일을 보면 </mapper namespace="spms.dao.projectdao"> 이라 되어 있고 SQL 문 id가 "selectList"이다. 이를 합치면 SQL ID는 spms.dao.projectdao.selectList 가 된다.
즉 이를 첫 번째 파라미터로 넣으면 SQL 맵퍼 파일에서 selectList라는 아이디를 갖는 <select> 태그를 가리키는 것이 된다.
추가로 SQL문을 실행하는데 값이 필요하다면 두 번째 매개변수로 값을 담은 객체를 넘겨주면 된다.
commit() / rollback() 메소드
DBMS는 INSERT, UPDATE, DELETE 문을 실행하면 그 작업 결과를 임시 데이터베이스에 보관한다. 클라이언트 요청이 있어야만 임시 데이터베이스의 작업물을 운영 데이터베이스에 반영한다. commit()은 임시 데이터베이스에 보관된 작업 결과를 운영 데이터베이스에 적용하라고 요청할 때 사용하는 메소드이다.
sqlSession.commit(); 부분이 이 명령어를 적용하였을 때 코드이다. 물론 SELECT 문은 값을 변경하는 것이 아니므로 commit() 메소드를 호출할 필요가 없다. rollback()은 임시 데이터베이스의 작업 결과를 운영 데이터베이스에 반영하지 않고 취소할 때 호출한다.
자동 커밋(Auto-commit)
commit()을 수동적으로 하지않고 자동으로 하고 싶다면 SqlSession 객체를 생성할 때 매개변수 값에 true를 넣어주면 된다. openSession()의 매개변수 값을 true로 지정하면 자동 커밋 객체를 반환한다. 자동 커밋으로 설정해 놓고 쓰면 편리하지만 트랜잭션을 다룰 수 없다.
SqlSession sqlSession = sqlSessionFactory.openSession(true);
💡MySqlProjectDao.xml - SQL mapper(맵퍼) 파일
<?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="spms.dao.ProjectDao">
<resultMap type="project" id="projectResultMap">
<id column="PNO" property="no" />
<result column="PNAME" property="title" />
<result column="CONTENT" property="content" />
<result column="STA_DATE" property="startDate"
javaType="java.sql.Date" />
<result column="END_DATE" property="endDate"
javaType="java.sql.Date" />
<result column="STATE" property="state" />
<result column="CRE_DATE" property="createdDate"
javaType="java.sql.Date" />
<result column="TAGS" property="tags" />
</resultMap>
<select id="selectList" resultMap="projectResultMap">
select PNO, PNAME, STA_DATE, END_DATE, STATE
from PROJECTS
order by PNO desc
</select>
<insert id="insert" parameterType="project">
insert into PROJECTS(PNAME,CONTENT,STA_DATE,END_DATE,STATE,CRE_DATE,TAGS)
values (#{title},#{content},#{startDate},#{endDate},0,now(),#{tags})
</insert>
<select id="selectOne" parameterType="int"
resultMap="projectResultMap">
select PNO, PNAME, CONTENT, STA_DATE, END_DATE, STATE, CRE_DATE, TAGS
from PROJECTS
where PNO=#{value}
</select>
<update id="update" parameterType="project">
update PROJECTS set
PNAME=#{title},
CONTENT=#{content},
STA_DATE=#{startDate},
END_DATE=#{endDate},
STATE=#{state},
TAGS=#{tags}
where PNO=#{no}
</update>
<delete id="delete" parameterType="int">
delete from PROJECTS
where PNO=#{value}
</delete>
</mapper>
매개변수 값 전달
#{프로퍼티명} 자리에 project 객체의 프로퍼티 값이 놓인다. 객체의 프로퍼티란 인스턴스 변수를 말하는 것이 아니라 getter/setter를 가리키는 용어이다. 프로퍼티 이름은 getter/setter 메소드의 이름에서 추출한다.
예를 들어 insert 부분으로 설명을 해보겠다. insert 부분에 #{title} 자리에는 project 객체의 getTitle() 반환값이 놓이고, #{content} 자리에는 project 객체의 getContent() 반환값이 놓인다.
Integer 객체 전달
Integer 클래스 같은 경우 프로퍼티를 의미하는 getter 메소드가 없기 때문에 이런 wrapper 객체로부터 값을 꺼낼 때는 아무 이름이나 사용해도 된다. delete 부분을 보면 Integer 객체의 값을 꺼내고자 'value'라는 이름을 사용한 것을 확인할 수 있다.
📌ApplicaitionContext 변경
https://yeo-computerclass.tistory.com/234
ApplicationContext를 통해 프로퍼티 파일과 어노테이션을 통해 객체를 생성하고 관리하였다. 이에 대해 자세하게 알고 싶으면 위 링크를 들어가 보면 된다.
SqlSessionFactory는 위 방식으로 생성할 수 없다. 별도로 SqlSessionFactory를 준비하여 ApplicationContext에 등록해주어야 한다. 외부에서 생성한 객체를 등록할 수 있게 ApplicationContext 파일을 변경해주어야 한다.
ApplicationContext
package spms.context;
import java.io.FileReader;
import java.lang.reflect.Method;
import java.util.Hashtable;
import java.util.Properties;
import java.util.Set;
import javax.naming.Context;
import javax.naming.InitialContext;
import org.reflections.Reflections;
import spms.annotation.Component;
// mybatis 적용에 필요한 변경
public class ApplicationContext {
Hashtable<String, Object> objTable = new Hashtable<String, Object>();
public Object getBean(String key) {
return objTable.get(key);
}
//외부에서 생성한 SqlSessionFactory를 등록할 수 있음
public void addBean(String name, Object obj) {
objTable.put(name, obj);
}
//기존 prepareAnnotationObjects() 메소드 이름을 바꾼 것
//외부에서 호출해야 하기 때문에 접근 제어자를 public으로 바꿈
//기존엔 매개변수가 없었지만, 어노테이션을 검색할 패키지 이름을 매개변수로 받음
public void prepareObjectsByAnnotation(String basePackage) throws Exception {
Reflections reflector = new Reflections(basePackage);
Set<Class<?>> list = reflector.getTypesAnnotatedWith(Component.class);
String key = null;
for (Class<?> clazz : list) {
key = clazz.getAnnotation(Component.class).value();
objTable.put(key, clazz.newInstance());
}
}
//기존 prepareObjects() 메소드 이름을 바꾼 것
//외부에서 호출해야 하기 때문에 접근 제어자를 public으로 바꿈
//매개변수로 Properties 객체를 직접 받는 대신, 프로퍼티 파일의 경로를 받아서 내부에서 Properties 객체 생성
public void prepareObjectsByProperties(String propertiesPath) throws Exception {
Properties props = new Properties();
props.load(new FileReader(propertiesPath));
Context ctx = new InitialContext();
String key = null;
String value = null;
for (Object item : props.keySet()) {
key = (String) item;
value = props.getProperty(key);
if (key.startsWith("jndi.")) {
objTable.put(key, ctx.lookup(value));
} else {
objTable.put(key, Class.forName(value).newInstance());
}
}
}
//외부에서 호출해야 하기 때문에 접근 제어자를 public으로 바꿈
public void injectDependency() throws Exception {
for (String key : objTable.keySet()) {
if (!key.startsWith("jndi.")) {
callSetter(objTable.get(key));
}
}
}
private void callSetter(Object obj) throws Exception {
Object dependency = null;
for (Method m : obj.getClass().getMethods()) {
if (m.getName().startsWith("set")) {
dependency = findObjectByType(m.getParameterTypes()[0]);
if (dependency != null) {
m.invoke(obj, dependency);
}
}
}
}
private Object findObjectByType(Class<?> type) {
for (Object obj : objTable.values()) {
if (type.isInstance(obj)) {
return obj;
}
}
return null;
}
}
생성자 제거
기존 코드에서 객체를 준비하고 의존 객체를 주입하는 일을 생성자에게 일괄처리 했었다. 하지만 이제는 외부에서 객체를 주입하는 경우도 고려해야 하기 때문에 일괄처리 방식을 개별처리 방식으로 변경해야 한다. 따라서 기존 코드에 있던 생성자를 제거하였다.
📌SqlSessionFactory 객체 준비
DAO 객체에 주입할 SqlSessionFactory 객체를 준비하여야 한다.
SqlSessionFactory 객체는 웹 어플리케이션이 시작할 때 생성되게 할 것이므로 ContextLoaderListener 파일에 작성하도록 하겠다.
ContextLoaderListener.java
package spms.listeners;
// SqlSessionFactory 객체 준비
import java.io.InputStream;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import spms.context.ApplicationContext;
@WebListener
public class ContextLoaderListener implements ServletContextListener {
static ApplicationContext applicationContext;
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
@Override
public void contextInitialized(ServletContextEvent event) {
try {
applicationContext = new ApplicationContext();
//SqlSessionFactory 객체 생성
String resource = "spms/dao/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//SqlSessionFactory 객체 등록
applicationContext.addBean("sqlSessionFactory", sqlSessionFactory);
//프로퍼티 파일 경로 알아내기
ServletContext sc = event.getServletContext();
String propertiesPath = sc.getRealPath(sc.getInitParameter("contextConfigLocation"));
//프로퍼타 피일의 내용에 따라 객체 생성
applicationContext.prepareObjectsByProperties(propertiesPath);
//어노테이션이 붙은 클래스 찾아 객체 생성
applicationContext.prepareObjectsByAnnotation("");
//ApplicationContext에서 관리하는 객체들 의존 객체를 주입
applicationContext.injectDependency();
} catch (Throwable e) {
e.printStackTrace();
}
}
@Override
public void contextDestroyed(ServletContextEvent event) {
}
}
SqlSessionFactory 객체 생성
SqlsessionFactory는 new 연산자로 객체를 생성할 수 없다. 단순한 객체는 new 연산자를 사용하여 직접 생성할 수 있지만 복잡한 객체는 다른 것을 통해 생성하도록 설계된 경우가 있다. 이런 식의 객체 생성 패턴을 빌더 패턴(Builder Pattern)이라 한다. SqlSessionFactory 클래스 이름에서 알 수 있듯이 SqlSession 객체를 만드는 Factory(공장) 클래스이다. 이 객체를 만들기 위해서는 SqlSessionFactoryBuilder 클래스를 이용하여 build()를 호출하여야 SqlSessionFactory 객체를 생성할 수 있다.
mybatis 설정 파일인 'mybatis-config.xml'은 SqlSessionFactory 객체를 생성할 때 사용되는 설계도면이라 생각하면 된다. 이때 build() 메소드의 매개변수 값으로 이 파일의 입력 스트림을 넘겨주어야 한다. 위 코드에서 이 파일의 입력 스트림을 얻기 위해 mybatis에서 제공하는 Resources 클래스를 사용했다. Resources의 getResourceAsStream() 메소드를 이용하면 Java 클래스 경로에 있는 파일의 입력 스트림을 손쉽게 얻을 수 있다. (mybatis 설정파일은 보통 Java 클래스 경로(CLASSPATH)에 위치한다.)
📌mybatis 설정 파일 준비
SqlSessionFactoryBuilder가 SqlSessionFactory 객체를 생성하려면 설계 도면이 필요하다. 즉 mybatis 설정 파일이 있어야 한다.
💡mybatis 설정 파일 구성
- DB 커넥션을 생성하는 데이터소스에 대한 정보
- 트랜잭션 관리자
- mybatis 동작을 제어하는 환경값
- SQL문이 저장된 맵퍼 파일의 경로
- etc
💡mybatis-config.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>
<properties resource="spms/dao/db.properties" />
<typeAliases>
<typeAlias type="spms.vo.Project" alias="project" />
</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="${username}" />
<property name="password" value="${password}" />
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="spms/dao/MySqlProjectDao.xml" />
</mappers>
</configuration>
💡db.properties
<properties> 태그에 명시한 프로퍼티 파일은 mybatis 설정 파일(mybatis-config.xml)에서 참조한다.
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/jwdb
username=ID
password=PASSWORD