MDC 란?
- MDC 는 java.util.Map 형식을 이용하여 클라이언트 특징적인 데이타를 저장하기 위한 메카니즘이다.
- 요즘 웹 어플리케이션은 멀티 스레드로 동작한다. 그렇기 때문에 서블릿은 어떤 클라이언트에 호출되더라도 같은 로그 기록을 남기는데, 이렇게 할 경우에 오류가 발생했을 때 어떤 클라이언트를 담당하는 스레드에서 오류가 발생했는지 알기 힘듭니다.
- 예를들어 서버 C에서 요청이 시작돼서 A에서 오류가 발생했다. 오류를 추적하기 어렵다. -> 그래서 requestID 등을 부여해서 로그를 적재시켜나가면 오류를 찾을 수 있을것이다.
- 현재 log4j 및 logback 만 MDC기능을 제공하고 있다.
- springboot 환경에서는 spring-boot-starter-web을 의존성으로 추가하면 logback이 기본으로 포함 되어있다.
MDC 클래스 메소드
// key 와 연관된 Object 를 얻음
public static Object get(String key);
// key 와 연관된 Object 를 저장
public static void put(String key, Object value);
// key 와 연관된 Object 매핑을 제거
public static void remove(String key);
구현
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean getFilterRegistrationBean() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean(new MDCLogFilter());
registrationBean.setUrlPatterns(Arrays.asList("/v1/*"));
return registrationBean;
}
}
- 위와 같이 WebConfig 파일에 MDCLogFilter 에 대한 설정을 한다.
- /v1 하위로 오는 모든 요청에 MDCLogFilter 를 적용하도록 설정하였다.
@Slf4j
public class MDCLogFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
String requestId = UUID.randomUUID().toString();
String requestUrl = req.getRequestURI();
String userAgent = req.getHeader("User-Agent");
String clientIP = getClientIP(req);
MDC.put("requestId", requestId);
MDC.put("requestUrl", requestUrl);
MDC.put("userAgent", userAgent);
MDC.put("clientIP", clientIP);
chain.doFilter(req, response);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
LOG.info("MDC filter initialize");
}
@Override
public void destroy() {
MDC.clear();
}
private String getClientIP(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
LOG.info("ip: {}", ip);
if(!StringUtils.hasLength(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
- 위와 같이 doFilter에서 UUID 로 requestId 를 할당받는다.
- destroy()에서 MDC 로그를 clear 해준다.
public enum MDCKeys {
/**
* MDC.put("requestId", requestId);
* MDC.put("requestUrl", requestUrl);
* MDC.put("userAgent", userAgent);
* MDC.put("clientIP", clientIP);
*/
REQUEST_ID("requestId"),
REQUEST_URL("requestUrl"),
USER_AGENT("userAgent"),
CLIENT_IP("clientIP");
MDCKeys(String propertyKey) {
this.propertyKey = propertyKey;
}
public String getPropertyKey() {
return propertyKey;
}
private String propertyKey;
}
- 위와 같이 MDCKeys 에 대한 enum 클래스를 정의한다.
@Slf4j
@RestController
public class MdcController {
@GetMapping("/v1/mdc")
public ResponseEntity getData() {
String requestId = MDC.get(MDCKeys.REQUEST_ID.getPropertyKey());
String clientIP = MDC.get(MDCKeys.CLIENT_IP.getPropertyKey());
String userAgent = MDC.get(MDCKeys.USER_AGENT.getPropertyKey());
LOG.info("requestId: {} clientIP: {} userAgent {}", requestId, clientIP, userAgent);
return ResponseEntity.ok("ok");
}
}
정리
- 트래픽이 많고 별도 쓰레드를 만들어서 요청 건을 가져오는 경우에는 쓰레드 번호가 중복되어 로그를 찾기 어려운 점이 있고, 하드코딩으로 특정 키 값을 로그에 남기기에는 꽤 공수가 든다.
- 하지만 위와 같이 MDC 를 적용한다면, 로그 추적의 효율성을 높일 수 있다.
- 위에서는 Filter로 구현을 했지만, 당연히 Interceptor를 이용해서 구현할 수도 있다.
REFERENCES