[LG U+ 유레카 4기] WEEK 08 - 데이터베이스 (7)

Soohwan Lim·2026년 5월 26일

유레카부트캠프

목록 보기
31/31
post-thumbnail

MyBatis 기초, Mapper XML, 동적 SQL


1. 오늘의 학습 흐름

  • MyBatis 개념 및 순수 JDBC와 비교
  • mybatis-config.xml 설정
  • Mapper XML 작성 (CRUD)
  • resultMap - 컬럼명과 필드명 매핑
  • SqlSession 관리 (MyBatisUtil)
  • 람다 + 콜백으로 SqlSession 템플릿 구현
  • Board CRUD + 페이지네이션 적용

2. MyBatis란

SQL과 Java 객체 매핑을 자동으로 처리해주는 퍼시스턴스 프레임워크다. 순수 JDBC에서 반복되던 Connection 관리, PreparedStatement, ResultSet, 자원 반납을 대신 처리한다.

순수 JDBC  → SQL + 연결 관리 + 결과 매핑 전부 개발자가 작성
MyBatis    → SQL만 XML에 작성, 나머지는 MyBatis가 처리

3. mybatis-config.xml - 전역 설정

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을 새로 만들던 문제가 해결된다.


4. Mapper XML - SQL 작성

namespace를 인터페이스 풀경로로 설정하면 해당 인터페이스의 메서드와 XML의 SQL이 id로 1:1 연결된다.

<mapper namespace="com.ureca.employee.model.dao.EmployeeDao">

resultMap - 컬럼명 ≠ 필드명 매핑

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"만 써도 자동 매핑된다.

CRUD 작성

<!-- 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 위험이 있어서 컬럼명/테이블명 같은 고정 구조에만 써야 한다.


5. MyBatisUtil - SqlSession 관리

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 템플릿 - 람다 + 콜백 패턴

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 패턴의 함수형 버전이다.


6. DAO 구현 - getMapper()

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 코드가 전부 사라지고 핵심 의도만 남는다.


7. Board - 페이지네이션 + 검색

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;
}

8. JDBC vs MyBatis 코드 비교

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가 처리한다.


9. 리뷰

SQL을 XML로 분리하는 게 처음엔 어색했다. 그런데 SQL이 복잡해질수록 Java 코드에 문자열로 박혀있는 것보다 XML에 따로 관리하는 게 훨씬 낫다는 걸 Board 예제에서 느꼈다.


10. 키워드 정리

MyBatis


11. 내일의 목표

  • SQLD 준비
profile
developer

0개의 댓글