실행 방법입니다. 외부 jar 는 필요 없습니다.
cd java-src\extension\10_logging
C:\project\jdk-21.0.8\bin\javac -encoding UTF-8 *.java
C:\project\jdk-21.0.8\bin\java -Dstdout.encoding=UTF-8 Main # 약 3초, logs/ 폴더 생성// Main.levelsAndHierarchy
APP.setLevel(Level.INFO);
REPO.setLevel(Level.FINE);
REPO.fine("SELECT * FROM member WHERE id=? ← repo 는 FINE 까지 출력");
SVC.fine("이 줄은 안 나옴 (service 는 app 의 INFO 상속)");
SVC.info("주문 생성 완료");
APP.warning("재고 임계치 도달");10:52:16.547 FINE [main] app.repo - SELECT * FROM member WHERE id=? ← repo 는 FINE 까지 출력
10:52:16.810 INFO [main] app.service - 주문 생성 완료
10:52:16.813 WARNING [main] app - 재고 임계치 도달
service 의 유효 레벨: INFO (자기 레벨은 null)REPO 는 자기 레벨(FINE)이 있어 그 값을 그대로 쓰고, SVC 는 자기 레벨이 없어 조상인 APP 의 INFO 를 상속받습니다. effective() 는 조상을 따라 올라가며 레벨이 있는 첫 로거를 찾는 함수입니다.
// Main.mdcPerRequest
for (String user : new String[]{"kim", "lee"}) {
Mdc.put("traceId", UUID.randomUUID().toString().substring(0, 8));
Mdc.put("user", user);
try {
SVC.info("주문 조회 시작");
REPO.fine("SELECT ... WHERE user=?");
SVC.info("주문 조회 완료 3건");
} finally {
Mdc.clear(); // 스레드 풀 재사용 시 이전 요청 값 오염 방지
}
}10:52:17.520 INFO [main] app.service - 주문 조회 시작 {traceId=79d2a28e, user=kim}
10:52:17.522 FINE [main] app.repo - SELECT ... WHERE user=? {traceId=79d2a28e, user=kim}
10:52:17.523 INFO [main] app.service - 주문 조회 완료 3건 {traceId=79d2a28e, user=kim}
10:52:17.525 INFO [main] app.service - 주문 조회 시작 {traceId=34738482, user=lee}
10:52:17.526 FINE [main] app.repo - SELECT ... WHERE user=? {traceId=34738482, user=lee}
10:52:17.526 INFO [main] app.service - 주문 조회 완료 3건 {traceId=34738482, user=lee}
10:52:17.526 INFO [main] app.service - 요청 밖: MDC 비어 있음kim 요청과 lee 요청의 traceId 가 서로 다르고, finally 의 clear() 덕분에 마지막 줄에는 MDC 값이 아예 없습니다. try/finally 로 감싸지 않으면 예외가 났을 때 clear() 가 건너뛰어져 다음 요청에 값이 남습니다.
// Mdc.wrap(Runnable)
public static Runnable wrap(Runnable r) {
Map<String, String> snapshot = copy();
return () -> {
Map<String, String> old = copy();
CTX.set(new LinkedHashMap<>(snapshot));
try { r.run(); } finally { CTX.set(old); }
};
}10:52:17.546 INFO [worker-1] app.service - 비동기 작업 (wrap 없음) ← traceId 사라짐
10:52:17.563 INFO [worker-1] app.service - 비동기 작업 (wrap) ← traceId 유지 {traceId=abc12345}wrap 없이 pool.submit 에 넘긴 작업은 worker-1 스레드 자신의 빈 MDC 를 보고, wrap 을 거친 작업은 제출 시점의 MDC 를 복사해 그대로 씁니다. Spring 의 TaskDecorator 가 @Async 메서드마다 이 복사를 자동으로 해 줍니다.
// Main.exceptionLogging
try {
Integer.parseInt("12a");
} catch (NumberFormatException e) {
SVC.severe("파싱 실패: " + e.getMessage() + " ← 어디서 났는지 모름");
SVC.log(Level.SEVERE, "파싱 실패", e); // Logback: log.error("파싱 실패", e)
}10:52:17.592 SEVERE [main] app.service - 파싱 실패: For input string: "12a" ← 어디서 났는지 모름
10:52:17.593 SEVERE [main] app.service - 파싱 실패
java.lang.NumberFormatException: For input string: "12a"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Integer.parseInt(Integer.java:661)첫 줄은 메시지만 남아 코드 어디서 예외가 났는지 알 수 없고, 둘째 줄은 Throwable 을 인자로 넘겨 스택 트레이스가 전부 남습니다. 실무에서는 catch 에서 로그를 찍고 그대로 다시 던지는 코드가 많은데, 이렇게 하면 호출 스택 위에서 같은 예외가 또 잡혀 로그에 2번, 3번 중복으로 남습니다.
// MaskingFilter.mask
public static String mask(String s) {
s = RRN.matcher(s).replaceAll("$1-*******");
s = CARD.matcher(s).replaceAll("$1-****-****-$2");
s = PHONE.matcher(s).replaceAll("$1-****-$2");
s = EMAIL.matcher(s).replaceAll("$1***$2");
return s;
}10:52:18.761 INFO [main] app.service - 회원 가입 rrn=901231-******* card=1234-****-****-3456 phone=010-****-5678 mail=ho***@test.comisLoggable 안에서 setMessage 로 원본 메시지를 마스킹한 버전으로 바꿔치기하고, 항상 true 를 반환해 로그 자체는 통과시킵니다. 이 필터는 콘솔·파일 등 어떤 핸들러 앞에도 붙일 수 있어 마스킹 로직을 한곳에 모을 수 있습니다.
Formatters.Line.format — 한 줄 포맷의 뼈대// Formatters.Line.format
StringBuilder sb = new StringBuilder()
.append(TS.format(Instant.ofEpochMilli(r.getMillis()))).append(' ')
.append(String.format("%-7s", r.getLevel().getName())).append(' ')
.append('[').append(Thread.currentThread().getName()).append("] ")
.append(r.getLoggerName()).append(" - ")
.append(formatMessage(r));
Map<String, String> mdc = Mdc.copy();
if (!mdc.isEmpty()) sb.append(' ').append(mdc);시각·레벨·스레드·로거 이름·메시지 순으로 한 줄에 고정해, grep·awk 로 필드를 쉽게 잘라낼 수 있게 합니다. MDC 가 비어 있지 않을 때만 뒤에 {key=value} 형태로 붙여, MDC 가 없는 일반 로그의 가독성을 해치지 않습니다.