06 mybatis interceptor
06 mybatis interceptor
1. mybatis
2. mybatis 인터셉터 흐름
1. 스프링 컨트롤러 / 서비스 호출
- 서비스에서 Mapper 메소드를 호출
- 예: userMapper.findById(10)
- 이때는 단순히 메소드 호출일 뿐, DB와 아직 연결되지 않음.
2. Mapper 프록시 호출
- 마이바티스는 Mapper 인터페이스를 동적으로 프록시로 만들어서, 호출이 들어오면 내부 SqlSession로 전달
- Mapper 메소드 호출 → SqlSession에서 해당 SQL ID와 파라미터 확인
- Note
- 인터페이스 자체는 실행하면 런타임 오류가 발생하므로 마이바티스에서 해당 인터페이스 호출을 하면 가로채서 해당하는 작업을 하려고 프록시 형태로 사용함.
- 즉 프록시는 호출된 메소드 이름과 파라미터를 읽고, 내부적으로 MappedStatement → BoundSql → Executor 순으로 SQL 실행을 연결하는 역할
3. MappedStatement 조회
- SQL ID를 기반으로 Mapper XML 또는 어노테이션에 정의된 SQL을 찾아 MappedStatement 객체 생성
- 여기에는 SQL 문자열, 파라미터 타입, 결과 매핑 정보 등이 담겨 있음
- Note
- MappedStatement 객체는 쉽게 말해서 SQL을 어떻게 써야하는지 파라미터는 어디에 뭘넣는지정보
- SQL 정보 / 파라미터 정보 / 결과 매핑 정보(ResultMap) / 기타 실행 옵션
4. BoundSql 생성
- 실제 SQL 실행 전, 파라미터를 바인딩할 준비를 함
- SQL 안의 #{} 플레이스홀더가 ?로 바뀌고, BoundSql에 파라미터 매핑 정보가 함께 저장됨
- Note
- BoundSql은 MappedStatement에서 정의된 SQL과 파라미터 매핑 정보를 바탕으로 실제 실행 가능한 형태의 SQL을 준비한 객체
5. Executor로 전달 → Statement 생성
- Executor가 Connection을 받아 PreparedStatement 혹은 CallableStatement 생성
- SQL이 DB에 맞게 준비됨 (? 포함 상태)
- 이 과정에서 인터셉터가 끼어들 수 있음 → SQL 확인, 파라미터 조작, 권한 필터 등
6. 파라미터 바인딩
- ? 위치에 실제 파라미터 값을 채움
- 마이바티스 내부에서 ParameterHandler가 담당
- 인터셉터를 사용하면 이 단계 전후에도 조작 가능
- Note
- 마이바티스 자체는 일반적으로 “실제 값이 채워진 SQL 문자열”을 직접 제공하지 않음
- 보통 로그용으로 만들 때는 BoundSql + ParameterMapping + ParameterObject를 활용해서 수동으로 치환하는 방식으로 완성된 SQL을 만들어야 함
7. SQL 실행
- PreparedStatement.executeQuery() 또는 executeUpdate() 호출
- DB에서 실제 결과가 나옴
8. 결과 매핑
- ResultSet을 자바 객체로 매핑 (ResultMap)
- Mapper 메소드의 반환 타입에 맞춰 객체, 리스트, Map 등으로 변환
9. Mapper 메소드 종료 → 서비스/컨트롤러로 반환
- 이제 결과 객체가 서비스로 넘어가고, 컨트롤러로 넘어가 화면에 전달 가능
3. BoundSql + ParameterMapping + ParameterObject
1. 흐름
1
2
3
4
5
1) XML / Annotation Mapper → SQL 템플릿
2) MappedStatement 생성 (SQL + 파라미터명 위치 정보 보유)
3) SqlSession 실행 시 → BoundSql 생성 (SQL + parameterMappings + parameterObject)
4) Executor가 PreparedStatement 생성
5) ParameterHandler가 parameterObject에서 값 꺼내 parameterMappings 순서대로 바인딩```
2. Source
1. SQL
1
2
3
4
5
<select id="findUser" parameterType="map" resultType="User">
SELECT id, name, age
FROM users
WHERE name = #{name} AND age > #{age}
</select>
2. Service
1
2
3
4
5
Map<String, Object> param = new HashMap<>();
param.put("name", "Hyeonjun");
param.put("age", 20);
userMapper.findUser(param);
3. BoundSql
1
2
3
4
5
6
7
BoundSql {
sql: "SELECT id, name, age
FROM users
WHERE name = ? AND age > ?"
parameterMappings: List<ParameterMapping>
parameterObject: Map<String, Object> param
}
4. ParameterMapping
1
2
3
4
parameterMappings = [
ParameterMapping { property: "name", javaType: String.class },
ParameterMapping { property: "age", javaType: Integer.class }
]
5. ParameterObject
1
2
3
4
parameterObject = {
"name" : "Hyeonjun",
"age" : 20
}
4. interceptor
1. interceptor override method
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
// 1) MappedStatement 꺼냄
MappedStatement ms = (MappedStatement) args[0];
// 2) ParameterObject 꺼냄 (select/update/insert/… 다 동일)
Object parameterObject = null;
if (args.length > 1) {
parameterObject = args[1];
}
// 3) 완성된 SQL 문자열 생성
String sql = getCompleteSql(ms, parameterObject);
System.out.println("실행되는 SQL = " + sql);
// 4) 다음 체인 실행
return invocation.proceed();
}
2. getCompleteSql
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public static String getCompleteSql(MappedStatement ms, Object parameterObject) {
BoundSql boundSql = ms.getBoundSql(parameterObject);
String sql = boundSql.getSql().replaceAll("\\s+", " ").trim(); // 보기 좋게 정리
List<ParameterMapping> paramMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
for (ParameterMapping paramMapping : paramMappings) {
String propertyName = paramMapping.getProperty();
Object value;
// 파라미터가 단일 값인지 DTO/Map인지 구분
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = ms.getConfiguration().newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
// 값이 문자열이면 작은따옴표 처리
String valueStr = (value instanceof String) ? "'" + value + "'" : String.valueOf(value);
// 첫 번째 ? 만 치환
sql = sql.replaceFirst("\\?", valueStr);
}
return sql;
}
5. Note - 중복 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 스레드별 마지막 실행 Mapper ID 저장
private static final ThreadLocal<String> lastMapperId = new ThreadLocal<>();
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
// 1) MappedStatement 꺼냄
MappedStatement ms = (MappedStatement) args[0];
// 2) ParameterObject 꺼냄
Object parameterObject = null;
if (args.length > 1) {
parameterObject = args[1];
}
// 3) 완성된 SQL 문자열 생성
String sql = getCompleteSql(ms, parameterObject);
// 4) 중복 로그 방지 (ThreadLocal 기반)
String currentId = ms.getId();
if (!currentId.equals(lastMapperId.get())) {
System.out.println("실행 Mapper: " + currentId);
System.out.println("실제 SQL: " + sql);
lastMapperId.set(currentId);
}
// 5) 다음 체인 실행
Object result = invocation.proceed();
// ThreadLocal 초기화
lastMapperId.remove();
return result;
- 마이바티스 내부 실행 흐름상 2번 호출됨 (?)
- 2025.11.09. 아무리 봐도 gpt도 여기저기 그냥 흐름상 발생한다는데 이해를 못하겠네.. 일단 너무 기술의 깊은 부분 같기도하고 JPA 위주로 사용하는 흐름이니 지금 중요한건 흐름을 파악하고 잘 활용하는 것이므로!, 조금 더 흐름타보고 언젠가 기회가 되면 이해할 수 있기를!!
- 스레드로컬(쓰레드 고유의 저장공간)이라는 변수를 사용해서 아이디값을 저장해놓고 같은거는 통과
This post is licensed under CC BY 4.0 by the author.