티스토리 뷰

회사에서 프로젝트 작업 중 갑자기 테이블들이 수정되면서 대부분의 테이블에 생성자, 생성일시, 수정자, 수정일시가 추가되었습니다. 로그인한 사용자의 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에 저장/조회 시 암/복호화할 때도 사용할 수 있을 것 같고 별도 로그를 남기고 싶을 때도 사용할 수 있을 것 같습니다. (추후에 암복호화도 적용하면 글을 수정해보겠습니다. 🙏)

 

참고

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
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
글 보관함