관리자 페이지 데이터 변경 기록 구현(2)

이전 포스팅에서는 Hibernate CustomEventListener를 사용하여 생성, 수정, 삭제 이벤트가 발생할 때마다 변경 이력을 자동으로 저장하는 방법을 살펴보았다. 이번 포스팅에서는 여러 모듈을 공유하는 멀티 모듈 프로젝트에서 특정 페이지(관리자 페이지)에서만 Hibernate CustomEventListener를 저장하는 방법에 대해 설명한다.

AdminLog Entity 구성

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "logs__admin_log")
public class AdminLogs {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(columnDefinition = "BIGINT UNSIGNED")
    @Comment("백오피스 로그 id")
    private BigInteger id;

    @Convert(converter = LogTypeConverter.class)
    @Comment("로그 타입 1. 생성 2. 수정 3. 삭제")
    private LogType logType;

    @Convert(converter = ControllerTypeConverter.class)
    @Comment("사용 컨트롤러 타입")
    private ControllerType controllerType;

    @Column(name = "request_url")
    private String baseUrl;

    @Column(name = "end_point")
    private String endPoint;

    @Column(name = "entity_name")
    private String entityName;

    @Column(name = "user_code")
    private String userCode;

    @Column(name = "logs", columnDefinition = "TEXT")
    @Convert(converter = ListToStringConverter.class)
    private List<String> logs;

    @CreationTimestamp
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy/MM/dd HH:mm:ss", timezone = "Asia/Seoul")
    @Column(nullable = false, columnDefinition = "TIMESTAMP(6) DEFAULT NOW(6)")
    @Comment("생성일")
    private Timestamp createdDateTime;
}

진행하는 프로젝트에서 관리자 페이지 변경 이력 저장 로그의 형태는 위와 같다. CustomEventLister의 return 값을 위한 AdminLog 생성 부분은 아래와 다

 adminLogsRepository.save(AdminLogs.builder()
                .logType(LogType.DELETE)
                .controllerType(RequestContextHolder.getCurrentControllerType())
                .baseUrl(RequestContextHolder.getCurrentBaseUrl())
                .endPoint(RequestContextHolder.getCurrentEndpoint())
                .entityName(entity.getClass().getSimpleName())
                .userCode(hardwareName)
                .logs(logs)
                .build());

RequestContextHolder 객체에서 controllerType, baseUrl ,endPoint 등을 받아온다.

PortInterceptor와 RequestContextHolder


public class RequestContextHolder {
    private static final ThreadLocal<Integer> currentPort = new ThreadLocal<>();
    private static final ThreadLocal<String> currentEndpoint = new ThreadLocal<>();
    private static final ThreadLocal<String> currentBaseUrl = new ThreadLocal<>();
    private static final ThreadLocal<ControllerType> currentControllerType = new ThreadLocal<>();

    public static void setCurrentPort(Integer port) {
        currentPort.set(port);
    }

    public static Integer getCurrentPort() {
        return currentPort.get();
    }

    public static void setCurrentEndpoint(String endpoint) {
        currentEndpoint.set(endpoint);
    }

    public static String getCurrentEndpoint() {
        return currentEndpoint.get();
    }

    public static void setCurrentBaseUrl(String baseUrl) {
        currentBaseUrl.set(baseUrl);
    }

    public static void setCurrentControllerType(ControllerType controllerType) {
        currentControllerType.set(controllerType);
    }

    public static ControllerType getCurrentControllerType() {
        return currentControllerType.get();
    }

    public static String getCurrentBaseUrl() {
        return currentBaseUrl.get();
    }

    public static void clear() {
        currentPort.remove();
        currentEndpoint.remove();
        currentBaseUrl.remove();
        currentControllerType.remove();
    }
}

RequestContextHolder 클래스는 스프링에서 제공하는 ThreadLocal을 이용하여 HTTP 요청과 관련된 정보를 저장하고 관리하는 클래스이다. ThreadLocal은 한 스레드 내에서 공유될 수 있는 변수를 제공하는 클래스이며, 스레드 별로 값을 저장하고 조회할 수 있는데, RequestContextHolder 클래스는 ThreadLocal을 이용하여 현재 HTTP 요청과 관련된 정보를 저장하고, 다른 클래스에서 필요할 때 값을 사용할 수 있도록 할 수 있게 한다.

RequestContextHolder 클래스는 currentPort, currentEndpoint, currentBaseUrl, currentControllerType 필드를 제공하며, 각각 현재 HTTP 요청의 포트, 엔드포인트, 베이스 URL, 그리고 컨트롤러 타입 정보를 저장한다. RequestContextHolder 클래스를 이용하여 HTTP 요청과 관련된 정보를 저장하고 공유함으로써, 이를 통해 AdminLog 클래스에 HTTP 정보를 저장할 수 있게 한다.

@Component
public class PortInterceptor implements HandlerInterceptor {
    @Value("${server.port}")
    private int serverPort;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String endpoint = request.getRequestURI();
        String baseUrl = request.getScheme() + "://" + request.getServerName() + ":" + serverPort;

        RequestContextHolder.setCurrentPort(serverPort);
        RequestContextHolder.setCurrentEndpoint(endpoint);
        RequestContextHolder.setCurrentBaseUrl(baseUrl);

        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();

            ControllerMarker controllerMarker = method.getAnnotation(ControllerMarker.class);
            if (controllerMarker != null) {
                RequestContextHolder.setCurrentControllerType(controllerMarker.value());
            }
        }
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        RequestContextHolder.clear();
    }
}

PortInterceptor 클래스는 HTTP 요청을 처리할 때 RequestContextHolder 에 각 필드를 설정하고, afterCompletion 메서드에서 필드 값을 초기화한다. 이 클래스는 HTTP 요청이 들어왔을 때, 요청의 endpoint, baseUrl, controllerType 등의 정보를 RequestContextHolder 에 저장하고, 이 정보를 CustomEventListener에서 사용하여 로그를 작성할 수 있게 한다.

WebMvcConfigurer

@Configuration
@RequiredArgsConstructor
@EnableWebMvc
public class WebMvcConfigurerimplements WebMvcConfigurer {
    private final PortInterceptor portInterceptor;
		...
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(portInterceptor);
    }
}

마지막으로 관리자 페이지 쪽 WebMvcConfigurer에 PortInterceptor를 주입해주면, 완료이다.

💡 Interceptor 사용 이유

PortInterceptorHandlerInterceptor 인터페이스를 구현한 클래스이다. HandlerInterceptor는 스프링에서 제공하는 인터셉터 기능의 인터페이스로, 컨트롤러가 요청을 받기 전과 후에 미리 지정된 로직을 실행시킬 수 있는 기능을 제공한다. PortInterceptorpreHandle 메서드에서 HTTP 요청의 endpoint, baseUrl, controllerType 등의 정보를 RequestContextHolder에 저장하고, afterCompletion 메서드에서 필드 값을 하는 기능을 한다.

PortInterceptor가 등록된 경우, HTTP 요청이 들어오면 preHandle 메서드가 호출되어 HTTP 요청의 정보를 RequestContextHolder에 저장하고, 이후 CustomEventListener에서는 RequestContextHolder를 이용하여 HTTP 정보를 가져와 로그를 작성할 수 있게 한다.

HandlerInterceptor는 컨트롤러가 요청을 받기 전과 후에 미리 지정된 로직을 실행시킬 수 있는 기능을 제공하며, HandlerInterceptor를 구현한 PortInterceptor는 HTTP 요청에 대한 정보를 RequestContextHolder에 저장하기 때문에 이 정보를 CustomEventListener에서 사용하여 로그를 작성할 수 있었다. 이와 달리 Filter는 컨트롤러가 요청을 받기 이전에 실행되는데, 이때는 컨트롤러단에서 생성되는 ControllerType 을 저장할 수 없었다. 또한 Filter는 모든 HTTP 요청에 대해 실행되므로, HandlerInterceptor를 사용하는 것이 더욱 효율적이라 생각했다.

Interceptor란?

인터셉터(Interceptor)는 스프링에서 제공하는 AOP(Aspect Oriented Programming) 구현체 중 하나로써, 컨트롤러가 요청을 받기 전과 후에 미리 지정된 로직을 실행시킬 수 있는 기능을 제공한다. 스프링은 프로그램의 흐름 중간에 개입하여 필요한 전처리(pre-processing)나 후처리(post-processing)를 수행할 수 있는 방법을 제공한다. 따라서, 인터셉터는 컨트롤러가 요청을 받기 전, 후 그리고 예외 발생 시점에 호출되는 기능이다.

Filter와 Interceptor의 차이

Filter는 Servlet에서 제공하는 인터페이스 중 하나로, Servlet의 요청과 응답에 대한 처리를 담당한다. 즉, 클라이언트의 요청을 받아서 서블릿이나 JSP, HTML 등으로 응답하기 전에 요청/응답 객체를 변형하거나, 기타 작업을 수행할 수 있는 기능을 제공하는 것이다.

Interceptor는 Dispatcher Servlet이 컨트롤러를 호출하기 전과 후에 요청과 응답에 대한 처리를 담당한다. 따라서, Filter가 Servlet에 종속적인 반면, Interceptor는 Spring Context에 종속적이다. 또한, Filter는 Request, Response 객체만 제공하지만, Interceptor는 HandlerInterceptor 인터페이스를 활용하여 컨트롤러를 호출하기 전, 후, 뷰가 렌더링 되기 전에도 작업을 할 수 있다.

즉, Filter와 Interceptor의 기능은 유사하지만, Filter는 Servlet에 종속적이고, Interceptor는 Spring Context에 종속적이며, 보다 세밀한 제어가 가능하다.

Filter와 Interceptor의 차이점 정리

  • Filter
    • Servlet에서 제공하는 인터페이스 중 하나
    • Servlet의 요청과 응답에 대한 처리를 담당
    • 클라이언트의 요청을 받아서 서블릿이나 JSP, HTML 등으로 응답하기 전에 요청/응답 객체를 변형하거나, 기타 작업을 수행할 수 있는 기능을 제공
    • Servlet에 종속적
    • Request, Response 객체만 제공
  • Intercepter
    • Spring에서 제공하는 AOP 구현체 중 하나
    • Dispatcher Servlet이 컨트롤러를 호출하기 전과 후에 요청과 응답에 대한 처리를 담당
    • 컨트롤러를 호출하기 전, 후, 뷰가 렌더링 되기 전에도 작업을 할 수 있음
    • Spring Context에 종속적
    • 보다 세밀한 제어 가능

정리

지금까지 RequestContextHolder 클래스와 PortInterceptor 클래스를 이용하여 관리자 페이지 데이터 변경 기록을 구현하는 방법에 대해 설명하였다.

  • RequestContextHolder 클래스는 ThreadLocal을 이용하여 HTTP 요청과 관련된 정보를 저장하고 관리하기 위해 생성했다.
  • PortInterceptor 클래스는 HTTP 요청을 처리할 때 RequestContextHolder 에 각 필드를 설정하고, afterCompletion 메서드에서 필드 값을 초기화한다. 이 클래스는 HTTP 요청이 들어왔을 때, 요청의 endpoint, baseUrl, controllerType 등의 정보를 RequestContextHolder에 저장하고, 이 정보를 CustomEventListener에서 사용하여 로그를 작성할 수 있게 하는 핵심이다.
  • Filter와 Interceptor의 기능이 유사하지만, Filter는 Servlet에 종속적이고, Intercepter는 Spring Context에 종속적이며, 보다 세밀한 제어가 가능하다.

결과물

관리자 페이지 데이터 변경 기록 구현(2)

댓글 남기기

Scroll to top