
MyBatis 기초, Mapper XML, 동적 SQL
SQL과 Java 객체 매핑을 자동으로 처리해주는 퍼시스턴스 프레임워크다. 순수 JDBC에서 반복되던 Connection 관리, PreparedStatement, ResultSet, 자원 반납을 대신 처리한다.
순수 JDBC → SQL + 연결 관리 + 결과 매핑 전부 개발자가 작성
MyBatis → SQL만 XML에 작성, 나머지는 MyBatis가 처리
MyBatis의 환경 설정 파일이다. DB 연결 정보, typeAlias, Mapper 위치를 설정한다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- typeAlias: 패키지 등록하면 클래스명 자체가 alias로 사용됨 -->
<!-- Employee 대신 com.ureca.employee.model.dto.Employee 전체 경로 안 써도 됨 -->
<typeAliases>
<package name="com.ureca.employee.model.dto"/>
<package name="com.ureca.board.model.dto"/>
</typeAliases>
<!-- DB 연결 정보 + Connection Pool -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED"> <!-- POOLED: 커넥션 풀 사용 -->
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/madangdb?..."/>
<property name="username" value="ureca"/>
<property name="password" value="ureca"/>
</dataSource>
</environment>
</environments>
<!-- Mapper XML 위치 등록 -->
<mappers>
<mapper resource="mapper/EmployeeMapper.xml"/>
<mapper resource="mapper/BoardMapper.xml"/>
</mappers>
</configuration>
type="POOLED"를 쓰면 MyBatis가 내부적으로 Connection Pool을 관리한다. 순수 JDBC에서 매번 Connection을 새로 만들던 문제가 해결된다.
namespace를 인터페이스 풀경로로 설정하면 해당 인터페이스의 메서드와 XML의 SQL이 id로 1:1 연결된다.
<mapper namespace="com.ureca.employee.model.dao.EmployeeDao">
DB 컬럼명(ename, sal)과 Java 필드명(name, salary)이 다를 때 resultMap으로 명시적으로 매핑한다.
<resultMap id="empMap" type="Employee">
<!-- id 태그: PK 컬럼 설정 -->
<id column="empno" property="empno"/>
<!-- result 태그: 일반 컬럼 설정 -->
<result column="ename" property="name"/>
<result column="sal" property="salary"/>
</resultMap>
컬럼명과 필드명이 같으면 resultMap 없이 resultType="Employee"만 써도 자동 매핑된다.
<!-- SELECT 전체 조회 -->
<select id="searchAll" resultMap="empMap">
SELECT empno, ename, sal FROM emp
</select>
<!-- SELECT 단건 조회 -->
<select id="search" parameterType="string" resultMap="empMap">
SELECT empno, ename, sal FROM emp WHERE empno = #{empno}
</select>
<!-- INSERT -->
<insert id="add" parameterType="Employee">
INSERT INTO emp(empno, ename, sal)
VALUES(#{empno}, #{name}, #{salary})
</insert>
<!-- UPDATE -->
<update id="update" parameterType="Employee">
UPDATE emp SET ename=#{name}, sal=#{salary}
WHERE empno = #{empno}
</update>
<!-- DELETE -->
<delete id="remove" parameterType="string">
DELETE FROM emp WHERE empno = #{empno}
</delete>
#{필드명}: PreparedStatement의 ?와 동일. 값을 바인딩하므로 SQL Injection 방지.
${필드명}: 문자열을 그대로 치환. SQL Injection 위험이 있어서 컬럼명/테이블명 같은 고정 구조에만 써야 한다.
SqlSessionFactory는 설정 파일을 읽어서 만드는 무거운 객체라 한 번만 생성하고 재사용한다. static 블록에서 초기화한다.
public class MyBatisUtil {
private static final SqlSessionFactory SQL_SESSION_FACTORY;
static {
try (InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml")) {
SQL_SESSION_FACTORY = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
throw new ExceptionInInitializerError("MyBatis 초기화 실패: " + e.getMessage());
}
}
}
SqlSession 열기 → 작업 → commit/rollback → close 패턴이 모든 DAO에서 반복된다. 이걸 executeInSession()으로 추출했다.
// 콜백 인터페이스 (함수형)
public interface SqlSessionCallback<T> {
T apply(SqlSession session) throws Exception;
}
// 템플릿 메서드
public static <T> T executeInSession(SqlSessionCallback<T> callback) throws SQLException {
SqlSession session = null;
try {
session = getSqlSessionFactory().openSession();
T result = callback.apply(session); // 실제 작업 위임
session.commit();
return result;
} catch (Exception e) {
if (session != null) session.rollback();
throw new SQLException(e);
} finally {
if (session != null) session.close();
}
}
JDBC의 DBUtil.close() 패턴과 같은 발상인데, 람다로 "작업 자체"까지 파라미터로 넘길 수 있어서 훨씬 강력하다. Template Method 패턴의 함수형 버전이다.
MyBatis의 getMapper()는 Mapper 인터페이스의 구현체를 동적으로 만들어준다. 개발자는 인터페이스만 정의하면 MyBatis가 XML의 SQL을 연결해서 실행한다.
public class EmployeeDaoMyBatisImp implements EmployeeDao {
// SELECT - 결과 반환
@Override
public Employee search(String empno) throws SQLException {
return executeInSession(session ->
session.getMapper(EmployeeDao.class).search(empno)
);
}
// SELECT ALL
@Override
public List<Employee> searchAll() throws SQLException {
return executeInSession(session ->
session.getMapper(EmployeeDao.class).searchAll()
);
}
// INSERT - 반환값 없으면 null
@Override
public void add(Employee emp) throws SQLException {
executeInSession(session -> {
session.getMapper(EmployeeDao.class).add(emp);
return null;
});
}
}
JDBC 버전과 비교하면 SQL, Connection, PreparedStatement, ResultSet, close 코드가 전부 사라지고 핵심 의도만 남는다.
PageBean은 페이지 번호, 검색 조건, 시작 인덱스를 담는 DTO다.
public class PageBean {
private String key; // 검색 조건 (id, title 등)
private String word; // 검색 단어
private int pageNo; // 현재 페이지
private int interval = 3; // 한 페이지에 보여줄 개수
public int getStart() {
if (pageNo > 1) return (pageNo - 1) * interval;
return 0;
}
}
getStart()가 LIMIT의 시작 인덱스를 계산한다. pageNo=2이고 interval=3이면 start=3이므로 LIMIT 3, 3이 된다.
BoardDao는 @Mapper 어노테이션을 붙여서 MyBatis가 인터페이스를 자동으로 인식하게 한다.
@Mapper
public interface BoardDao {
int getBoardNo() throws SQLException;
void insertBoard(Board board) throws SQLException;
Board search(String no) throws SQLException;
List<Board> searchAll(PageBean bean) throws SQLException;
int count(PageBean bean) throws SQLException;
void updateBoard(Board board) throws SQLException;
void deleteBoard(String no) throws SQLException;
}
JDBC (search 메서드)
public Employee search(String empno) throws SQLException {
Connection con = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
con = dbutil.getConnection();
stmt = con.prepareStatement("SELECT empno, ename, sal FROM emp WHERE empno=?");
stmt.setString(1, empno);
rs = stmt.executeQuery();
if (rs.next()) {
Employee emp = new Employee();
emp.setEmpno(rs.getString("empno"));
emp.setName(rs.getString("ename"));
emp.setSalary(rs.getInt("sal"));
return emp;
}
} finally {
dbutil.close(rs, stmt, con);
}
return null;
}
MyBatis (search 메서드)
public Employee search(String empno) throws SQLException {
return executeInSession(session ->
session.getMapper(EmployeeDao.class).search(empno)
);
}
XML에 SQL 한 줄 + Java에 한 줄. 나머지는 MyBatis가 처리한다.
SQL을 XML로 분리하는 게 처음엔 어색했다. 그런데 SQL이 복잡해질수록 Java 코드에 문자열로 박혀있는 것보다 XML에 따로 관리하는 게 훨씬 낫다는 걸 Board 예제에서 느꼈다.
MyBatis