티스토리 뷰
회사에서 프로젝트 작업 중 갑자기 테이블들이 수정되면서 대부분의 테이블에 생성자, 생성일시, 수정자, 수정일시가 추가되었습니다. 로그인한 사용자의 Id를 기반으로 데이터를 insert, update, delete할 때 해당 내용들을 추가해줘야 했고 이를 위해 Interceptor를 구현하기로 했습니다.
CommonAuditing 추상 클래스 생성 후 필요한 Model 들에서 이를 상속받아 추가하는 방법도 고민했지만 이미 어느정도 작업이 진행되어 있어서 전체 DTO, Domain, Model을 수정하는 것은 무리가 있다고 생각했고 그나마 나은 방법으로 Mapper와 xml 파일만 수정할 수 있는 방법을 고민하여 Interceptor를 구현하는 것으로 결정했습니다.
먼저 다음과 같이 Interceptor를 구현해줍니다.
@Slf4j
@Intercepts({
@Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }),
@Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class })
})
public class MybatisExecuteInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
String method = invocation.getMethod().getName();
Object[] args = invocation.getArgs();
Object param = args[1];
if ("update".equals(method)) {
setAuditing(param);
return invocation.proceed();
} else if ("query".equals(method)) {
// TODO 조회 시 할 일
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Interceptor.super.plugin(target);
}
@Override
public void setProperties(Properties properties) {
Interceptor.super.setProperties(properties);
}
private void setAuditing(Object param) {
log.info("[MybatisExecuteInterceptor] update param = {}", param);
SessionDto session = SessionScopeUtil.getContextSession(); // 로그인한 사용자 정보 조회
if (param instanceof MapperMethod.ParamMap<?>) {
MapperMethod.ParamMap<Object> newParam = (MapperMethod.ParamMap<Object>) param;
newParam.put("createdBy", session.getStaffId());
newParam.put("createAt", LocalDateTime.now());
newParam.put("lastModifiedBy", session.getStaffId());
newParam.put("lastModifiedAt", LocalDateTime.now());
}
}
}
org.apache.ibatis.plugin.Interceptor 인터페이스를 구현하면 interceptor 라는 메서드를 오버라이딩 해야합니다.
이 메서드의 인자엔 Invocation 객체가 제공되는데 여기에 파라미터로 전달된 값과 호출된 xml 태그에 대한 메타 정보들을 얻을 수 있습니다.
@Signature 애노테이션을 좀 더 살펴보면 다음과 같습니다.
- type
- Executor 라는 인터페이스는 Mybatis의 xml 파일에 작성된 SQL을 실행합니다. 해당 인스턴스 내부를 보면 각 mybatis method 시그니처 정보를 볼 수 있습니다
- method
- insert / update / delete 가 실행되면 Excutor의 update 라는 메서드를 호출하며 select 는 query 라는 메서드를 호출합니다.
- args
- 공통적으로 MapperStatement라는 객체가 Object[] 타입의 인덱스 0번에 필수로 저장됩니다. 여기에 xml 메타 정보가 담겨 있습니다.
위 코드에서는 insert / update / delete 시 전달된 파라미터들을 받아 해당 파라미터들 뒤에 필요한 값들을 Map의 key, value 형태로 넣어줍니다. (어떻게 할지 고민했는데 내용은 뒤에서...)
이후 사용하고 있던 mybatis-config.xml에 plugin으로 interceptor를 추가해줍니다.
<?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>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<setting name="callSettersOnNulls" value="true"/>
<setting name="defaultFetchSize" value="100"/>
</settings>
<plugins>
<plugin interceptor="me.hanbee.test.interceptor.MybatisExecuteInterceptor"/>
</plugins>
</configuration>
이렇게 적용한 Interceptor를 사용하는 방법은 다음과 같습니다.
// Mapper 클래스
@Mapper
public interface MemberMapper {
...
int save(@Param("m") Member member);
...
}
// xml 파일
<insert id="save" useGeneratedKeys="true" keyProperty="m.id">
INSERT INTO member(email, name, created_by, create_at)
VALUES (#{m.email}, #{m.name}, #{createdBy}, #{createAt})
</insert>
이렇게 하면 비즈니스 또는 프레젠테이션 레이어의 객체들 또는 Model 객체 자체에 데이터 저장, 수정을 위한 생성자/생성일시/수정자/수정일시에 대한 프로퍼티 값들을 세팅할 필요가 없어지고 Mapper와 xml 쿼리 부분만 수정을 통해 원하는 작업을 수행할 수 있습니다.
💡 @Param("dto") 를 꼭 넣어줘야할지
- 기존에 insert 시 파라미터 하나만 넘기는 경우 별도의 @Param 애노테이션을 사용하지 않고 xml 파일에서도 #{dto.xxx} 대신 #{xxx}를 바로 사용했습니다.
- 이 경우에는 interceptor에서 파라미터를 확인했을 때 바로 객체가 들어오게 되고 이 경우에 객체 내부에 동적으로 필드를 추가하는 것은 조금 어렵지 않을까 판단했습니다.
- param = Member(email=xxx@gmail.com, ...)
- 따라서 기본적으로 xml 로 전달되는 파라미터에 @Param 애노테이션을 붙여서 interceptor에서 확인했을 때 Map 형태로 받을 수 있도록 하였고 추가적인 파라미터를 Map에 추가하는 방식으로 개발하였습니다.
- param = {m=Member(), param1=Member(), createdBy="xxx", ... }
- @Param 을 사용하지 않고 객체 하나를 파라미터로 받는 경우 이를 Map으로 변환하고 해당 Map에 추가 파라미터를 넣는 방향으로 진행을 해볼까 생각했지만 직렬화/역직렬화 이슈가 있었고
- 객체를 그대로 사용한다면 각 Model(DTO, Domain 등)에서 추가 컬럼에 대한 필드를 가지고 있거나 상속을 받는 코드를 추가해줘야 합니다. (코드 수정이 많아 선택하지 않았습니다.)
❗️이슈
- ObjectMapper 사용하여 객체를 Map으로 변경 시 LocalDateTime 변환 이슈 존재
지금까지 MyBatis Interceptor를 사용하여 공통으로 사용되는 필드에 값을 추가하는 방법에 대해 알아보았습니다. 이슈들이 있었지만... 이부분은 추후에 더 나은 방향을 찾아보도록 하겠습니다.
해당 Interceptor는 특정 필드를 DB에 저장/조회 시 암/복호화할 때도 사용할 수 있을 것 같고 별도 로그를 남기고 싶을 때도 사용할 수 있을 것 같습니다. (추후에 암복호화도 적용하면 글을 수정해보겠습니다. 🙏)
참고
'기타' 카테고리의 다른 글
[항해플러스 3기] 시작하는 마음 (1) | 2023.12.02 |
---|---|
[TypeORM] Node + Express 환경에서 TypeORM 사용하기 (0) | 2023.11.29 |
[Socket.io] NodeJS + Express 기반 소캣 통신 서버 만들기 1 (0) | 2023.11.13 |
[Kafka] Kafka 개념 및 용어 정리 (1) | 2023.11.10 |
[Redis] Redis username, password 설정하기 (0) | 2023.10.30 |
- Total
- Today
- Yesterday
- Algorithm
- programmers
- ECR
- CodeDeploy
- java
- AWS
- CodeCommit
- sort
- 조합
- DFS
- ionic
- Dynamic Programming
- 에라토스테네스의 체
- 수학
- BFS
- cloudfront
- EC2
- permutation
- Baekjoon
- 순열
- SWIFT
- array
- Combination
- spring
- string
- search
- 프로그래머스
- map
- 소수
- CodePipeline
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |