1. MyBatis 소개
- MyBatis는 자바 애플리케이션에서 관계형 데이터베이스에 액세스하기 위한 SQL 매퍼 프레임워크입니다.
- 기존 JDBC의 복잡성을 추상화하고, Spring의 JdbcTemplate보다 더 많은 기능을 제공합니다.
1.1 MyBatis의 주요 특징
- SQL과 자바 코드의 분리: SQL 쿼리를 XML 파일에 따로 작성하여 자바 코드와 분리
- 간결한 코드: JDBC의 반복적인 코드를 제거하고 핵심 비즈니스 로직에 집중 가능
- 강력한 동적 쿼리: 조건에 따라 SQL을 동적으로 구성하는 기능 제공
- 객체-결과 매핑: 데이터베이스 결과를 자바 객체로 쉽게 매핑
- 확장성: 플러그인 시스템을 통한 기능 확장
1.2 JdbcTemplate과 비교
JdbcTemplate에 비해 MyBatis가 갖는 가장 큰 장점:
- SQL을 XML에 편리하게 작성 가능
- 동적 쿼리를 매우 효과적으로 구현 가능
- 복잡한 객체 관계 매핑 지원
- 다양한 데이터베이스 벤더 지원
2. MyBatis 시작하기
2.1 의존성 추가
Gradle
implementation 'org.mybatis:mybatis:3.5.13'
Maven
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>
2.2 기본 구성 요소
MyBatis의 핵심 컴포넌트:
- SqlSessionFactoryBuilder: 설정 파일을 읽어 SqlSessionFactory 생성
- SqlSessionFactory: SqlSession 인스턴스를 생성하는 팩토리
- SqlSession: SQL 명령을 실행하는 주요 인터페이스
- Mapper Interface: SQL 매핑 파일과 자바 인터페이스를 연결
- Mapper XML 파일: SQL 쿼리와 매핑 정보를 포함하는 XML 파일
2.3 기본 설정
SqlSessionFactory 생성
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession 사용
try (SqlSession session = sqlSessionFactory.openSession()) {
// SQL 명령 실행
User user = session.selectOne("org.mybatis.example.UserMapper.findById", 1);
// 트랜잭션 커밋
session.commit();
}
3. MyBatis 설정
MyBatis의 설정은 XML 기반 설정 파일을 통해 이루어집니다.
3.1 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>
<!-- 설정 요소들 -->
</configuration>
3.2 주요 설정 요소
- properties: 외부 속성 파일 설정 (DB 연결 정보 등)
- settings: MyBatis 동작 방식 조정
- typeAliases: 자바 타입 별칭 설정
- typeHandlers: 자바-JDBC 타입 변환 관리
- environments: 다양한 환경(개발, 테스트, 운영) 설정
- environment: 특정 환경 구성
- transactionManager: 트랜잭션 관리 방식
- dataSource: 데이터베이스 연결 정보
- environment: 특정 환경 구성
- mappers: SQL 매퍼 파일 위치 지정
3.3 설정 예제
<configuration>
<properties resource="application.properties"/>
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<typeAliases>
<package name="com.example.domain"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mappers/UserMapper.xml"/>
<mapper resource="mappers/ProductMapper.xml"/>
</mappers>
</configuration>
4. Mapper 인터페이스와 XML
MyBatis에서는 Mapper 인터페이스와 XML 파일을 통해 SQL을 관리합니다.
4.1 Mapper 인터페이스
@Mapper
public interface UserMapper {
User findById(Long id);
List<User> findAll();
void save(User user);
void update(User user);
void delete(Long id);
}
- 인터페이스에
@Mapper
애노테이션을 붙여 MyBatis가 인식하게 함 - 메서드 호출 시 매핑된 XML의 SQL이 실행됨
- Spring과 함께 사용할 경우 자동 주입 가능
4.2 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.example.mapper.UserMapper">
<select id="findById" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
<select id="findAll" resultType="User">
SELECT * FROM users
</select>
<insert id="save" parameterType="User">
INSERT INTO users (name, email)
VALUES (#{name}, #{email})
</insert>
<update id="update" parameterType="User">
UPDATE users
SET name = #{name},
email = #{email}
WHERE id = #{id}
</update>
<delete id="delete">
DELETE FROM users WHERE id = #{id}
</delete>
</mapper>
4.3 Result Maps
복잡한 객체 매핑을 위해 ResultMap을 사용합니다:
<resultMap id="userResultMap" type="User">
<id property="id" column="user_id"/>
<result property="username" column="user_name"/>
<result property="email" column="user_email"/>
<association property="profile" javaType="Profile">
<id property="id" column="profile_id"/>
<result property="bio" column="bio"/>
</association>
</resultMap>
<select id="selectUserWithProfile" resultMap="userResultMap">
SELECT u.id as user_id, u.username as user_name, u.email as user_email,
p.id as profile_id, p.bio as bio
FROM users u
LEFT JOIN profiles p ON u.id = p.user_id
WHERE u.id = #{id}
</select>
5. 동적 SQL
MyBatis의 가장 강력한 기능 중 하나는 동적 SQL 생성 기능입니다.
5.1 조건부 SQL (if)
<select id="findByCondition" resultType="User">
SELECT * FROM users
WHERE 1=1
<if test="name != null">
AND name LIKE #{name}
</if>
<if test="email != null">
AND email = #{email}
</if>
</select>
5.2 다중 조건 (choose, when, otherwise)
<select id="findUsers" resultType="User">
SELECT * FROM users
WHERE 1=1
<choose>
<when test="name != null">
AND name LIKE #{name}
</when>
<when test="email != null">
AND email = #{email}
</when>
<otherwise>
AND active = true
</otherwise>
</choose>
</select>
5.3 반복 처리 (foreach)
<select id="findByIds" resultType="User">
SELECT * FROM users
WHERE id IN
<foreach item="id" collection="list" open="(" separator="," close=")">
#{id}
</foreach>
</select>
5.4 SQL 조각 재사용 (sql, include)
<sql id="userColumns">
id, name, email, created_at
</sql>
<select id="selectUser" resultType="User">
SELECT
<include refid="userColumns"/>
FROM users WHERE id = #{id}
</select>
6. MyBatis 플러그인 시스템
MyBatis는 확장 가능한 플러그인 시스템을 제공합니다.
6.1 플러그인 개요
- 플러그인은 SQL 실행 과정 중 특정 지점에서 호출을 가로채 기능을 확장
- 다음 객체의 메소드를 가로챌 수 있음:
- Executor: SQL 실행을 담당 (update, query, commit, rollback 등)
- ParameterHandler: 파라미터 바인딩 담당
- ResultSetHandler: 결과 처리 담당
- StatementHandler: SQL 문 준비 및 실행 담당
6.2 플러그인 작성 예시
@Intercepts({
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)
})
public class ExamplePlugin implements Interceptor {
private Properties properties;
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 원본 StatementHandler 가져오기
StatementHandler handler = (StatementHandler) invocation.getTarget();
// 원본 SQL 가져오기
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
// SQL 로깅 또는 수정
System.out.println("Original SQL: " + sql);
// 원본 메소드 실행
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
this.properties = properties;
}
}
6.3 플러그인 등록
<plugins>
<plugin interceptor="com.example.ExamplePlugin">
<property name="someProperty" value="100"/>
</plugin>
</plugins>
6.4 플러그인 활용 사례
- SQL 실행 로깅
- 성능 모니터링
- 페이징 처리 자동화
- 감사(Audit) 기능 구현
- 테넌트 ID 자동 삽입 (멀티테넌트 애플리케이션)
- 데이터 암호화/복호화
7. Spring과 MyBatis 통합
Spring Framework와 MyBatis를 함께 사용하면 더 강력한 애플리케이션을 구축할 수 있습니다.
7.1 의존성 추가
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
7.2 Spring Boot 설정
# application.yml
mybatis:
mapper-locations: classpath:mappers/**/*.xml
type-aliases-package: com.example.domain
configuration:
map-underscore-to-camel-case: true
7.3 Mapper 스캔
@SpringBootApplication
@MapperScan("com.example.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
8. 실제 구현 예제
8.1 도메인 클래스
public class User {
private Long id;
private String name;
private String email;
private LocalDateTime createdAt;
// getter, setter, constructor
}
8.2 Mapper 인터페이스
@Mapper
public interface UserMapper {
@Select("SELECT * FROM users WHERE id = #{id}")
User findById(Long id);
List<User> findByCondition(UserSearchCriteria criteria);
@Insert("INSERT INTO users (name, email) VALUES (#{name}, #{email})")
@Options(useGeneratedKeys = true, keyProperty = "id")
void save(User user);
}
8.3 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.example.mapper.UserMapper">
<select id="findByCondition" resultType="User">
SELECT * FROM users
<where>
<if test="name != null">
name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="email != null">
AND email = #{email}
</if>
<if test="createdAfter != null">
AND created_at >= #{createdAfter}
</if>
</where>
ORDER BY created_at DESC
<if test="limit != null">
LIMIT #{limit}
</if>
<if test="offset != null">
OFFSET #{offset}
</if>
</select>
</mapper>
8.4 서비스 클래스
@Service
public class UserService {
private final UserMapper userMapper;
public UserService(UserMapper userMapper) {
this.userMapper = userMapper;
}
public User findById(Long id) {
return userMapper.findById(id);
}
public List<User> findByCondition(UserSearchCriteria criteria) {
return userMapper.findByCondition(criteria);
}
@Transactional
public void createUser(User user) {
userMapper.save(user);
}
}