티스토리 뷰
Mapped Diagnostic Context (MDC)
로깅 라이브러리에서 개발자의 로깅과 디버깅을 위해 지원하는 기술입니다.
MDC를 사용하면 여러 스레드가 동시다발적으로 소스 코드를 실행할 때, 어떤 스레드에 의해 소스 코드가 실행되었는지에 대한 맥락을 로그 메시지에 추가할 수 있습니다. 특히 클라이언트-서버 아키텍처에서 여러 클라이언트가 동시에 한 서버로 요청을 보내면 각 요청에 대한 로그 메시지가 무작위로 출력될텐데요. 각 로그의 출처가 어떤 클라이언트인지 구분해야 할 때 MDC가 매우 유용합니다.
본 포스팅은 블로그 주인장의 'MDC는 어떻게 Request 단위로 값을 저장할까?'라는 궁금증을 계기로 작성되었는데요. MDC의 니즈와 사용법에 대해서는 이미 자세히 설명된 자료가 있어 아래 링크로 대체하겠습니다.
MDC는 어떻게 클라이언트의 요청 단위로 값을 저장하고 있을까?
오해하지 말자. MDC는 스레드 단위로 값을 저장한다.
정확히 말하자면 MDC는 클라이언트의 요청이 아니라 스레드 단위로 키와 값을 저장하고 있습니다. 많은 개발자가 애용하는 Java 기반의 서버 프레임워크인 Spring MVC는 1개의 요청을 1개의 스레드가 처리하는 Thread-per-request 모델로 동작하고 있는데요. Servlet을 통해 들어온 클라이언트의 요청은 Controller Layer를 거쳐 Repository Layer를 통해 Database에 접근할 때까지 모두 동일한 단일 스레드를 사용합니다.
따라서 MDC는 스레드 단위로 키값을 저장하고 있지만, 만일 스프링 MVC를 사용하고 있다면 하나의 요청을 하나의 스레드가 처리하기 때문에 MDC가 마치 클라이언트의 요청 단위로 값을 저장하는 것처럼 느낄 수 있습니다.
스레드 세상인 자바에서 만능적인 ThreadLocal
MDC의 저장 방식을 직접 소스 코드를 뜯어 알아볼게요.
package org.slf4j;
public class MDC {
static MDCAdapter mdcAdapter;
public static void put(String key, String val) throws IllegalArgumentException {
if (key == null) {
throw new IllegalArgumentException("key parameter cannot be null");
} else if (mdcAdapter == null) {
throw new IllegalStateException("MDCAdapter cannot be null. See also http://www.slf4j.org/codes.html#null_MDCA");
} else {
mdcAdapter.put(key, val);
}
}
public static String get(String key) throws IllegalArgumentException {
if (key == null) {
throw new IllegalArgumentException("key parameter cannot be null");
} else if (mdcAdapter == null) {
throw new IllegalStateException("MDCAdapter cannot be null. See also http://www.slf4j.org/codes.html#null_MDCA");
} else {
return mdcAdapter.get(key);
}
}
public static void remove(String key) throws IllegalArgumentException {
if (key == null) {
throw new IllegalArgumentException("key parameter cannot be null");
} else if (mdcAdapter == null) {
throw new IllegalStateException("MDCAdapter cannot be null. See also http://www.slf4j.org/codes.html#null_MDCA");
} else {
mdcAdapter.remove(key);
}
}
public static void clear() {
if (mdcAdapter == null) {
throw new IllegalStateException("MDCAdapter cannot be null. See also http://www.slf4j.org/codes.html#null_MDCA");
} else {
mdcAdapter.clear();
}
}
}
개발자가 특정 값을 저장하고 조회할 때 참조하는 MDC 클래스는 key-value의 관리를 MDCAdapter에 위임하고 있습니다. MDCAdapter는 라이브러리에 따라 여러 구현체가 존재하는데, slf4j의 BasicMDCAdapter.kt를 기준으로 살펴볼게요.
public class BasicMDCAdapter implements MDCAdapter {
private InheritableThreadLocal<Map<String, String>> inheritableThreadLocal;
public void put(String key, String val) {
if (key == null) {
throw new IllegalArgumentException("key cannot be null");
}
Map<String, String> map = inheritableThreadLocal.get();
if (map == null) {
map = new HashMap<String, String>();
inheritableThreadLocal.set(map);
}
map.put(key, val);
}
public String get(String key) {
Map<String, String> map = inheritableThreadLocal.get();
if ((map != null) && (key != null)) {
return map.get(key);
} else {
return null;
}
}
...
}
BasicMDCAdapter는 key-value를 ThreadLocal의 구현체인 InheritableThreadLocal에 저장하고 있습니다.
사실 MDC의 값 저장 방식은 개발자가 ThreadLocal에 접근해 값을 저장하는 방식과 거의 일치합니다. 대신 MDC는 logback.xml 설정 파일이나 로그 메시지에서 저장한 값을 더 편리하게 출력할 수 있도록 여러 유틸을 제공하고 있어요.
스레드 로컬에 직접 접근하는 로깅 방식과 라이브러리를 통한 로깅 방식을 비교하고 싶다면 아래 자료를 참고해주세요.
ThreadLocal
하나의 스레드 안에서만 저장하고 공유할 수 있는 로컬 변수를 제공합니다.
다른 스레드에게 영향을 미치지 않고, 주로 각 스레드에 고유 식별자를 만들어 로컬에 저장하고 싶을 때 사용합니다.
import java. util. concurrent. atomic. AtomicInteger;
// 사용 예시
public class ThreadId {
// Atomic integer containing the next thread ID to be assigned
private static final AtomicInteger nextId = new AtomicInteger(0);
// Thread local variable containing each thread's ID
private static final ThreadLocal<Integer> threadId =
new ThreadLocal<Integer>() {
@Override protected Integer initialValue() {
return nextId. getAndIncrement();
}
};
// Returns the current thread's unique ID, assigning it if necessary
public static int get() {
return threadId. get();
}
}
따라서 MDC를 사용할 때 아래 두가지를 주의하세요.
1. 모든 작업이 끝나면 clear()를 명시적으로 호출하세요.
ThreadLocal에 저장된 값들은 스레드가 종료되기 전까지 사라지지 않습니다. 특히 Tomcat을 사용하는 경우, 작업이 끝난 스레드는 종료되지 않고 스레드 풀에 반환되어 재사용되기 때문에 모든 작업이 끝난 후에는 clear()를 명시적으로 해주는 것이 좋습니다.
2. 새로 생성한 스레드에게 자동으로 MDC가 전달되지 않아요.
// 예시 코드
fun main() {
val logger = LoggerFactory.getLogger("Main")
MDC.put("key", "hyeonae")
val parentValue = MDC.get("key")
logger.info("parentValue = $parentValue")
thread {
val childValue = MDC.get("key")
logger.info("childValue = $childValue")
}
}
/**
[Result]
16:17:10.918 [main] INFO Main -- parentValue = hyeonae
16:17:10.921 [Thread-0] INFO Main -- childValue = null
**/
비동기 방식을 지원하기 위해서는 스레드의 생성이 필수적인데요.
생성된 스레드에 MDC를 전달하고 싶다면 아래와 같이 기존 스레드의 ContextMap을 복제해 사용해야 합니다.
fun main() {
val copyOfContextMap = MDC.getCopyOfContextMap()
thread {
MDC.setContextMap(copyOfContextMap)
}
}
스레드를 생성할 때마다 ContextMap 복제 코드가 중복된다면 TaskDecorator를 커스텀해 자동화할 수 있어요.
@Configuration
public class AsyncConfig {
@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setTaskDecorator(new LoggingTaskDecorator());
return taskExecutor;
}
}
class LoggingTaskDecorator: TaskDecorator {
override fun decorate(runnable: Runnable): Runnable {
val copyOfContextMap = MDC.getCopyOfContextMap()
return Runnable {
MDC.setContextMap(copyOfContextMap)
runnable.run()
}
}
}
마무리하며
MDC 원리에 대한 궁금증으로 ThreadLocal의 존재를 알게되었는데요.
이번 글을 마무리하고 다음에는 인증/인가, 영속성 컨텍스트 등 ThreadLocal의 활용 사례에 대해 알아보겠습니다.
참고자료
https://www.slf4j.org/api/org/slf4j/MDC.html
https://logback.qos.ch/manual/mdc.html
https://hudi.blog/slf4j-mapped-diagnotics-context/
'SPRING' 카테고리의 다른 글
Spring Data JPA의 Fetch 전략과 Fetch 조인 비교하기 (1) | 2023.06.14 |
---|---|
단위 테스트는 얼마나 격리되어야 하나요? (Feat. 테스트 더블) (0) | 2023.05.25 |
스프링에서 예외 처리하기 (@ExceptionHandler, @ControllerAdvice, @ResponseStatus) (0) | 2023.03.09 |
[토비의 스프링] 6. AOP (2) (0) | 2022.12.29 |
[토비의 스프링] 6. AOP(1) (0) | 2022.12.29 |
- Total
- Today
- Yesterday
- SELECT #SELECTFROM #WHERE #ORDERBY #GROUPBY #HAVING #EXISTS #NOTEXISTS #UNION #MINUS #INTERSECTION #SQL #SQLPLUS
- 백준27211
- 사용자ID
- 백준
- 리눅스
- Linux
- 버추억박스에러
- 리눅스cron
- 버추억박스오류
- 쇼미더코드
- linuxgedit
- linuxawk
- awk프로그램
- api문서
- OnActivityForResult
- GithubAPI
- Baekjoon27219
- 백준27219
- cat
- virtualbox
- cron시스템
- E_FAIL
- baekjoon
- linuxtouch
- GitHubAPIforJava
- whatis
- Baekjoon27211
- 코테
- linux파일
- atq
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |