마이바티스 스프링 부트 통합 및 플러그인 메커니즘
1. 서론
마이바티스(MyBatis)는 매우 유연한 지속성 프레임워크로, JDBC를 내부적으로 캡슐화하여 개발자가 SQL 문 자체에만 집중할 수 있게 합니다. 드라이버 로딩, 연결 생성, Statement 생성 등 복잡한 과정에 신경 쓸 필요가 없습니다. 풍부한 설정 옵션과 강력한 SQL 매핑 기능을 제공할 뿐만 아니라, 플러그인 메커니즘을 지원하여 SQL 실행 라이프사이클에 맞춰 사용자 정의 로직을 삽입할 수 있습니다. 본 문서에서는 스프링 부트 프로젝트에서 마이바티스를 통합하는 방법과 마이바티스 플러그인 메커니즘의 원리와 실제 적용 사례를 상세히 분석합니다.
2. 스프링 부트에서 마이바티스 통합
2.1 데이터베이스 준비
본 예제에서는 MySQL 5.7 데이터베이스를 사용합니다.
create database if not exists mybatis_integration;
use mybatis_integration;
create table member(
id int unsigned primary key auto_increment comment '식별자',
name varchar(100) comment '이름',
age tinyint unsigned comment '나이',
gender tinyint unsigned comment '성별, 1:남성, 2:여성',
phone varchar(11) comment '전화번호'
) comment '회원 정보 테이블';
insert into member(id, name, age, gender, phone) VALUES (null,'양백응왕',55,'1','18800000001');
insert into member(id, name, age, gender, phone) VALUES (null,'금모사왕',45,'1','18800000002');
insert into member(id, name, age, gender, phone) VALUES (null,'청응왕',38,'1','18800000003');
insert into member(id, name, age, gender, phone) VALUES (null,'자산용왕',42,'2','18800000004');
insert into member(id, name, age, gender, phone) VALUES (null,'광명좌사',37,'1','18800000005');
insert into member(id, name, age, gender, phone) VALUES (null,'광명우사',48,'1','18800000006');
2.2 의존성 설정
필요한 Maven 의존성을 pom.xml에 추가합니다. 부모 프로젝트는 스프링 부트 부모 프로젝트를 상속받습니다. MySQL 드라이버, 마이바티스 스타터, 웹 스타터, Lombok 애너테이션이 포함됩니다.
<?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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.7.10</version>
</parent>
<groupId>com.example</groupId>
<artifactId>mybatis-integration</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>MyBatisIntegration</name>
<description>스프링 부트와 마이바티스 통합 예제</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<!-- 마이바티스 코어 라이브러리 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<!-- 스프링 부트 웹 스타터 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 스프링 부트 테스트 스타터 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- JUnit 테스팅 프레임워크 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<!-- Lombok 애너테이션 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<!-- 마이바티스 스프링 부트 스타터 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!-- MySQL 커넥터 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
2.3 애플리케이션 설정
application.yml 파일에서 데이터베이스 연결 정보를 설정합니다. 포트 번호는 8081로 지정합니다.
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/mybatis_integration?useSSL=false&serverTimezone=UTC
username: root
password: password
server:
port: 8081
mybatis:
# 마이바티스 전역 설정 파일 경로
config-location: classpath:mybatis-config.xml
# 개발 환경에서 콘솔 로그 출력
logging:
level:
com.example.mybatisintegration.mapper:
debug
2.4 계층별 구현
마이바티스는 XML 설정과 애너테이션 방식 두 가지로 데이터베이스 작업을 구현할 수 있습니다. 먼저 두 방식에서 공통으로 사용되는 코드를 살펴보겠습니다.
2.4.1 도메인 모델
데이터베이스 테이블의 필드를 매핑할 엔티티 클래스를 생성합니다.
package com.example.mybatisintegration.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor // 매개변수 없는 생성자
@AllArgsConstructor // 모든 매개변수를 받는 생성자
public class Member {
private Integer id;
private String name;
private Short age;
private Short gender;
private String phone;
}
2.4.2 서비스 계층
서비스 인터페이스를 정의합니다.
package com.example.mybatisintegration.service;
import com.example.mybatisintegration.model.Member;
import java.util.List;
public interface MemberService {
Member fetchMemberById(Integer id);
List<Member> fetchAllMembers();
String removeMemberById(Integer id);
String registerMember(Member member);
String modifyMember(Member member);
}
서비스 구현체를 작성합니다.
package com.example.mybatisintegration.service.impl;
import com.example.mybatisintegration.mapper.MemberMapper;
import com.example.mybatisintegration.model.Member;
import com.example.mybatisintegration.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class MemberServiceImpl implements MemberService {
@Autowired
private MemberMapper memberMapper;
@Override
public Member fetchMemberById(Integer id) {
return memberMapper.fetchById(id);
}
@Override
public List<Member> fetchAllMembers() {
return memberMapper.fetchAll();
}
@Override
public String removeMemberById(Integer id) {
memberMapper.deleteById(id);
return "삭제 완료";
}
@Override
public String registerMember(Member member) {
memberMapper.insertMember(member);
return "등록 완료";
}
@Override
public String modifyMember(Member member) {
memberMapper.updateMember(member);
return "수정 완료";
}
}
2.4.3 컨트롤러 계층
컨트롤러에서 서비스를 주입받아 API 엔드포인트를 구현합니다.
package com.example.mybatisintegration.controller;
import com.example.mybatisintegration.model.Member;
import com.example.mybatisintegration.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/members")
public class MemberController {
@Autowired
private MemberService memberService;
@GetMapping("/{id}")
public Member fetchMember(@PathVariable Integer id){
return memberService.fetchMemberById(id);
}
@GetMapping
public List<Member> fetchAllMembers() {
return memberService.fetchAllMembers();
}
@PostMapping
public String registerMember(@RequestBody Member member){
return memberService.registerMember(member);
}
@DeleteMapping("/{id}")
public String removeMember(@PathVariable Integer id){
return memberService.removeMemberById(id);
}
@PutMapping
public String modifyMember(@RequestBody Member member){
return memberService.modifyMember(member);
}
}
2.4.4 애너테이션 기반 매퍼
애너테이션을 사용한 매퍼 인터페이스입니다.
package com.example.mybatisintegration.mapper;
import com.example.mybatisintegration.model.Member;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface MemberMapper {
@Select("select * from member where id = #{id}")
Member fetchById(Integer id);
@Select("select * from member")
List<Member> fetchAll();
@Delete("delete from member where id = #{id}")
void deleteById(Integer id);
@Insert("insert into member(name,age,gender,phone) values(#{name},#{age},#{gender},#{phone})")
void insertMember(Member member);
@Update("update member set name = #{name},age = #{age},gender = #{gender},phone = #{phone} where id = #{id}")
void updateMember(Member member);
}
2.4.5 XML 기반 매퍼
XML 설정을 사용한 방식입니다. 매퍼 인터페이스와 XML 매핑 파일이 필요합니다.
매퍼 인터페이스:
package com.example.mybatisintegration.mapper;
import com.example.mybatisintegration.model.Member;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface MemberMapper {
Member fetchById(Integer id);
List<Member> fetchAll();
void deleteById(Integer id);
void insertMember(Member member);
void updateMember(Member member);
}
XML 매핑 파일:
<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="com.example.mybatisintegration.mapper.MemberMapper">
<select id="fetchById" resultType="com.example.mybatisintegration.model.Member">
select * from member where id = #{id}
</select>
<select id="fetchAll" resultType="com.example.mybatisintegration.model.Member">
select * from member
</select>
<delete id="deleteById" parameterType="int">
delete from member where id = #{id}
</delete>
<insert id="insertMember" parameterType="com.example.mybatisintegration.model.Member">
insert into member(name,age,gender,phone) values(#{name},#{age},#{gender},#{phone})
</insert>
<update id="updateMember" parameterType="com.example.mybatisintegration.model.Member">
update member set name = #{name},age = #{age},gender = #{gender},phone = #{phone} where id = #{id}
</update>
</mapper>
2.4.6 전역 설정 파일
mybatis-config.xml 파일에서 전역 설정을 정의합니다.
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<!-- 설정 옵션 -->
<settings>
<!-- 지연 로딩 전역 스위치 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 지연 로딩 시 모든 연관 속성 로드 -->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 단일 SQL 다중 결과 집합 허용 -->
<setting name="multipleResultSetsEnabled" value="true"/>
<!-- 열 레이블 대신 열 이름 사용 -->
<setting name="useColumnLabel" value="true"/>
<!-- JDBC 생성 키 값 지원 -->
<setting name="useGeneratedKeys" value="true"/>
<!-- 기본 실행기 유형 설정 -->
<setting name="defaultExecutorType" value="SIMPLE"/>
<!-- 쿼리 타임아웃 설정 -->
<setting name="defaultStatementTimeout" value="25"/>
<!-- 자동 카멜 케이스 매핑 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<!-- 환경 설정 -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis_integration?useSSL=false&serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="password"/>
</dataSource>
</environment>
</environments>
!-- 매퍼 파일 -->
<mappers>
<mapper resource="mapper/MemberMapper.xml"/>
</mappers>
</configuration>
참고: 전역 설정 파일은 생략하고 application.yml에 모든 설정을 포함할 수 있습니다. 이때 로딩 순서는 다음과 같습니다: mybatis-config.xml → application.yml의 마이바티스 설정 → 자바 설정(Java Config). 우선순위는 로딩 순서와 반대입니다.
3. 마이바티스 플러그인 메커니즘
3.1 플러그 개요
대부분의 오픈소스 프레임워크는 개발자가 기능을 확장할 수 있도록 확장점을 제공합니다. 예를 들어 스프링 프레임워크의 `BeanPostProcessor` 인터페이스는 개발자가 객체 초기화 전후에 작업을 수행할 수 있게 합니다. 마이바티스도 확장점을 제공하며, 이를 플러그인 메커니즘이라고 부릅니다. 본질적으로 플러그인은 JDK 동적 프록시와 책임 연쇄 패턴(Chain of Responsibility)의 결합체입니다.
마이바티스가 제공하는 주요 확장점은 다음과 같습니다:
- Executor: SQL 실행기 (update, query, commit,rollback 메서드)
- StatementHandler: SQL 구문 생성기 (prepare, parameterize, batch, update, query 등 메서드)
- ParameterHandler: 파라미터 처리기 (getParameterObject, setParameters 등 메서드)
- ResultSetHandler: 결과 집합 처리기 (handleResultSets, handleOutputParameters 등 메서드)
3.2 플러그 구현 단계
마이바티스 플러그을 구현하는 주요 단계는 다음과 같습니다:
Interceptor인터페이스 구현@Intercepts및@Signature애너테이션을 사용하여 플러그인 지점 정의- 마이바티스 전역 설정 파일에서 플러그인 등록
3.2.1 Interceptor 인터페이스 구현
먼저 org.apache.ibatis.plugin.Interceptor 인터페이스를 구현해야 합니다:
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import java.util.Properties;
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class SamplePlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 대상 메서드 실행 전 로직
Object result = invocation.proceed();
// 대상 메서드 실행 후 로직
return result;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 플러그인 속성 설정
}
}
위 코드에서 @Intercepts 애너테이션은 플러그인으로 지정할 클래스를 표시하고, 플러그인이 가로챌 메서드를 지정합니다. intercept 메서드는 플러그인의 핵심 로직이며, plugin 메서드는 대상 객체의 프록시를 생성하는 데 사용됩니다.
3.2.2 플러그인 등록
마이바티스 설정 파일(mybatis-config.xml)에서 플러그인을 등록합니다:
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<plugins>
<plugin interceptor="com.example.plugin.QueryTimePlugin">
<property name="threshold" value="1000"/>
</plugin>
</plugins>
</configuration>
3.3 커스텀 플러그 예제
3.3.1 쿼리 실행 시간 측정 플러그
실제 플러그인 예제로 SQL 쿼리 실행 시간을 측정하는 방법을 살펴보겠습니다. 이 플러그은 JDBC 연결 생성과 프리파레드 스테이트먼트 생성 시간도 포함합니다.
3.3.1.1 쿼리 실행 시간 측정 플러그 구현
package com.example.mybatisintegration.plugin;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Properties;
/**
* 쿼리 실행 시간을 측정하는 플러그
*/
@Intercepts({
@Signature(
type = Executor.class,
method = "update",
args = {MappedStatement.class, Object.class}),
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class QueryTimePlugin implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(QueryTimePlugin.class);
// 느린 쿼리 임계값(밀리초)
private long slowQueryThreshold = 1000;
@Override
public Object intercept(Invocation invocation) throws Throwable {
// SQL 실행 관련 정보 가져오기
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
String sqlId = mappedStatement.getId();
long startTime = System.currentTimeMillis();
try {
// 원본 메서드 실행
return invocation.proceed();
} finally {
long costTime = System.currentTimeMillis() - startTime;
// 로그 기록
if (costTime > slowQueryThreshold) {
logger.warn("느린 쿼리 실행 시간: {}ms > {}ms, SQL ID: {}",
costTime, slowQueryThreshold, sqlId);
} else {
logger.debug("쿼리 실행 시간: {}ms, SQL ID: {}", costTime, sqlId);
}
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 설정에서 임계값 읽기
String threshold = properties.getProperty("threshold");
if (threshold != null) {
this.slowQueryThreshold = Long.parseLong(threshold);
}
}
}
3.3.1.2 쿼리 실행 시간 측정 플러그 등록
전역 설정 파일에 플러그을 등록합니다:
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<plugins>
<plugin interceptor="com.example.mybatisintegration.plugin.QueryTimePlugin">
<property name="threshold" value="500"/>
</plugin>
</plugins>
</configuration>
3.3.2 쿼리 결과 암호화 플러그
쿼리 결과에서 전화번호 필드를 MD5로 암호화하는 플러그을 구현해 보겠습니다.
3.3.2.1 암호화 유틸리티 클래스
package com.example.mybatisintegration.util;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* 다양한 해시 알고리즘을 제공하는 유틸리티 클래스
*/
public class CryptoUtil {
private static final char[] HEX_CHARS = "0123456789abcdef".toCharArray();
/**
* 문자열의 MD5 해시값 계산
* @param input 입력 문자열
* @return 32자 소문자 MD5 해시값
*/
public static String md5(String input) {
return digest(input, "MD5");
}
/**
* 문자열의 SHA-256 해시값 계산
* @param input 입력 문자열
* @return 64자 소문자 SHA-256 해시값
*/
public static String sha256(String input) {
return digest(input, "SHA-256");
}
/**
* 일반 해시 계산 메서드
* @param input 입력 문자열
* @param algorithm 알고리즘 이름
* @return 해시 문자열
*/
private static String digest(String input, String algorithm) {
try {
MessageDigest md = MessageDigest.getInstance(algorithm);
byte[] bytes = md.digest(input.getBytes(StandardCharsets.UTF_8));
return bytesToHex(bytes);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
/**
* 바이트 배열을 16진수 문자열로 변환
* @param bytes 바이트 배열
* @return 16진수 문자열
*/
private static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int i = 0; i < bytes.length; i++) {
int v = bytes[i] & 0xFF;
hexChars[i * 2] = HEX_CHARS[v >>> 4];
hexChars[i * 2 + 1] = HEX_CHARS[v & 0x0F];
}
return new String(hexChars);
}
}
3.3.2.2 결과 집합 처리기 구현
package com.example.mybatisintegration.plugin;
import com.example.mybatisintegration.model.Member;
import com.example.mybatisintegration.util.CryptoUtil;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import java.sql.Statement;
import java.util.List;
import java.util.Properties;
/**
* 결과 집합을 암호화하는 처리기
*/
public class EncryptingResultSetHandler implements ResultSetHandler {
private final ResultSetHandler originalHandler;
public EncryptingResultSetHandler(ResultSetHandler originalHandler) {
this.originalHandler = originalHandler;
}
@Override
public List<Object> handleResultSets(Statement stmt) throws Exception {
// 원본 처리기로 결과 집합 처리
List<Object> results = this.originalHandler.handleResultSets(stmt);
// 결과 집합에서 전화번호 필드 암호화
if (results instanceof List) {
for (Object item : results) {
if (item instanceof Member) {
Member member = (Member) item;
String encryptedPhone = CryptoUtil.md5(member.getPhone());
member.setPhone(encryptedPhone);
}
}
}
return results;
}
// 다른 메서드들은 기본 구현 사용
@Override
public void handleOutputParameters(java.sql.CallableStatement cs) throws Exception {
originalHandler.handleOutputParameters(cs);
}
}
3.3.2.3 결과 집합 처리 플러그 구현
package com.example.mybatisintegration.plugin;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;
import org.springframework.stereotype.Component;
import java.sql.Statement;
import java.util.Properties;
/**
* 결과 집합 처리를 가로채는 플러그
*/
@Component
@Intercepts({@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})})
public class ResultSetEncryptionPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Statement stmt = (Statement) invocation.getArgs()[0];
// 원본 ResultSetHandler 가져오기
ResultSetHandler originalHandler = (ResultSetHandler) invocation.getTarget();
// 암호화 처리기 생성
EncryptingResultSetHandler encryptingHandler = new EncryptingResultSetHandler(originalHandler);
// 암호화 처리기로 결과 집합 처리
return encryptingHandler.handleResultSets(stmt);
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 플러그 속성 설정 (필요 시)
}
}
3.3.2.4 결과 암호화 플러그 등록
전역 설정 파일에 플러그을 등록합니다:
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<plugins>
<plugin interceptor="com.example.mybatisintegration.plugin.ResultSetEncryptionPlugin"/>
</plugins>
</configuration>
3.4 플러그 메커니즘 소스 코드 분석
플러그 메커니즘의 원리와 구현 방법을 이해한 후, 소스 코드를 깊이 있게 분석해 보겠습니다. 분석 과정에서 다음 세 가지 질문을 중심으로 진행합니다:
- 객체는 어떻게 인스턴스화되는가?
- 플러그 인스턴스 객체가 어떻게 인터셉터 체인에 추가되는가?
- 컴포넌트 객체의 프록시 객체가 어떻게 생성되는가?
3.4.1 플러그 설정 정보 로딩 및 파싱
플러그을 정의한 후 마이바티스에 어떻게 전달할까요? 전역 설정 파일에 등록합니다. 이에 해당하는 파싱 코드는 XMLConfigBuilder#pluginsElement에서 발생합니다:
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 인터셉터 클래스 가져오기
String interceptorClass = child.getStringAttribute("interceptor");
// 설정된 속성 Properties로 가져오기
Properties properties = child.getChildrenAsProperties();
// 설정 파일에 지정된 플러그 클래스의 전체 이름으로 리플렉션을 통해 초기화
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptorClass).getDeclaredConstructor().newInstance();
// 속성을 Interceptor 객체에 설정
interceptorInstance.setProperties(properties);
// 설정 클래스의 InterceptorChain에 추가
configuration.addInterceptor(interceptorInstance);
}
}
}
다음과 같은 작업을 수행합니다:
- plugins 태그 아래 각 plugin 태그를 순회합니다
- 클래스 정보를 기반으로 Interceptor 객체를 생성합니다 3. setProperties 메서드를 호출하여 속성을 설정합니다
- Interceptor 객체를 Configuration 클래스의 InterceptorChain에 추가합니다
- SQL 로깅: SQL 문장 및 실행 시간을 기록하여 디버깅과 최적화에 도움을 줍니다.
- 파라미터 검증 및 수정: SQL 실행 전 파라미터를 검증하고 수정하여 데이터의 정확성과 보안을 보장합니다.
- 쿼리 결과 처리: 쿼리 결과를 처리하여 데이터 마스킹, 형식 변환 등을 수행합니다.
- 성능 모니터링: SQL 실행 시간, 실행 횟수 등을 모니터링하여 시스템 성능을 최적화하는 데 도움을 줍니다.
- 플러그 구현은 간결하고 효율적이어야 하며, 불필요한 성능 오버헤드를 추가하지 않도록 합니다.
- 플러그 구성은 합리적으로 해야 하며, 과도한 사용으로 인해 코드 복잡도가 증가하지 않도록 합니다.
- 플러그 실행 순서는 설정 파일의 순서에 따라 결정되므로 필요에 따라 순서를 조정할 수 있습니다.
3.4.2 프록시 객체 생성
이전에 언급했듯이, 플러그 메커니즘은 마이바티스의 네 가지 주요 컴포넌트의 메서드를 가로챌 수 있습니다. 이제 구체적으로 메서드 가로채기를 통해 프록시 객체가 생성되는 방법을 살펴보겠습니다.
Executor 프록시 객체(Configuration#newExecutor):
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// Executor 프록시 객체 생성 로직
return (Executor) interceptorChain.pluginAll(executor);
}
ParameterHandler 프록시 객체(Configuration#newParameterHandler):
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject,
BoundSql boundSql) {
// ParameterHandler 생성
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement,
parameterObject, boundSql);
// ParameterHandler 프록시 객체 생성 로직
return (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
}
ResultSetHandler 프록시 객체(Configuration#newResultSetHandler):
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds,
ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler,
resultHandler, boundSql, rowBounds);
// ResultSetHandler 프록시 객체 생성 로직
return (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
}
StatementHandler 프록시 객체(Configuration#newStatementHandler):
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement,
Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// 라우팅 기능을 가진 StatementHandler 생성,
// MappedStatement의 StatementType에 따라 해당 StatementHandler 생성
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject,
rowBounds, resultHandler, boundSql);
return (StatementHandler) interceptorChain.pluginAll(statementHandler);
}
소스 코드를 살펴보면 모든 프록시 객체 생성이 InterceptorChain#pluginAll 메서드를 통해 이루어지는 것을 알 수 있습니다. InterceptorChain#pluginAll 내부는 Interceptor#plugin 메서드를 순회하여 프록시 객체를 생성하고, 생성된 프록시 객체를 다시 target에 할당합니다. 여러 개의 인터셉터가 있는 경우, 생성된 프록시 객체가 다른 프록시 객체에 의해 프록시되어 프록시 체인을 형성합니다. 실행 시 모든 인터셉터의 가로채기 로직 코드가 순차적으로 실행됩니다.
// org.apache.ibatis.plugin.InterceptorChain
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
// org.apache.ibatis.plugin.Interceptor
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
public static Object wrap(Object target, Interceptor interceptor) {
// 1. 해당 인터셉터가 가로챌 모든 인터페이스 및 해당 메서드 파싱
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
// 2. 대상 객체가 구현한 모든 가로채기 대상 인터페이스 가져오기
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
// 3. 대상 객체가 가로채기 대상 인터페이스를 구현한 경우 프록시 객체 생성 및 반환
if (interfaces.length > 0) {
// JDK 동적 프록시 방식으로 구현
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
// 대상 객체가 가로채기 대상 인터페이스를 구현하지 않은 경우 원본 객체 반환
return target;
}
3.4.3 가로채기 로직 실행
마이바티스 프레임워크에서 Executor, ParameterHandler, ResultSetHandler 및 StatementHandler의 메서드를 실행할 때 실제로는 프록시 객체의 해당 메서드를 실행합니다. 따라서 메서드 실행은 실제로 InvocationHandler#invoke 메서드(Plugin 클래스가 InvocationHandler 인터페이스를 구현)를 호출하는 것입니다. 다음은 Plugin#invoke 메서드입니다:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
주의: 동일한 컴포넌트 객체의 동일한 메서드를 여러 인터셉터가 가로챌 수 있습니다. 설정 파일에서 앞에 있는 인터셉터가 먼저 프록시되지만, 실행 시에는 가장 바깥쪽 인터셉터가 먼저 실행됩니다. 즉, 포장 순서와 실행 순서는 반대입니다.
3.5 플러그 메커니즘의 적용 시나리오 및 주의사항
3.5.1 적용 시나리오
3.5.2 주의사항
4. 결론
본 문서에서는 스프링 부트 프로젝트에서 마이바티스를 통합하는 방법과 마이바티스 플러그 메커니즘의 원리와 실제 적용 사례를 상세히 분석했습니다. 마이바티스 플러그 메커니즘은 SQL 실행의 각 단계에 사용자 정의 로직을 삽입할 수 있는 유연한 방식을 제공하여 마이바티스의 확장성을 크게 향상시킵니다. 본 문서에서는 플러그 메커니즘의 소스 코드를 다루었으며, 향후 문서에서는 마이바티스의 핵심 소스 코드를 상세히 분석하고 핵심 컴포넌트의 역할과 실행 시점을 분석할 예정입니다.