이 레슨의 네 파일에 직접 코드를 덧붙이는 문제 5개입니다. 정답을 보기 전에 먼저 힌트만 보고 스스로 작성해 보세요.
Mdc 에 AutoCloseable 을 반환하는 scope(key, value) 를 추가해, try (var s = Mdc.scope("traceId", id)) { ... } 형태로 쓰면 블록을 나갈 때 자동으로 해당 키를 지우도록 하세요. 힌트: close() 안에서 remove(key) 를 호출하는 익명 클래스나 람다를 반환합니다.
public static AutoCloseable scope(String key, String value) {
put(key, value);
return () -> remove(key);
}finally { Mdc.clear(); } 를 매번 손으로 쓰는 대신, try 선언부에 넣기만 하면 됩니다. 여러 키를 동시에 여닫아야 하면 scope 를 여러 번 중첩해서 씁니다.
MaskingFilter 에 Authorization: Bearer xxx 형태의 토큰을 마스킹하는 패턴을 추가하고, 토큰의 앞 6자만 남기도록 하세요. 힌트: Bearer 뒤 문자열을 그룹으로 잡고 앞부분만 남기는 치환을 씁니다.
static final Pattern TOKEN = Pattern.compile("Bearer (\\w{6})\\w+");
public static String mask(String s) {
s = TOKEN.matcher(s).replaceAll("Bearer $1***");
s = RRN.matcher(s).replaceAll("$1-*******");
// ... 이하 기존 카드·휴대폰·이메일 마스킹
return s;
}mask("Authorization: Bearer abcdef123456") 를 호출하면 Bearer abcdef*** 가 나와야 정답입니다.
인증 토큰은 그대로 노출되면 세션 탈취로 이어질 수 있어 반드시 마스킹 대상입니다.
요청 시작에 Mdc.put("startNanos", String.valueOf(System.nanoTime())) 을 넣어 두고, Formatters.Line.format 이 현재 시각과의 차이를 밀리초로 계산해 줄 끝에 덧붙이도록 하세요. 힌트: MDC 값은 문자열이므로 Long.parseLong 으로 바꿔 계산합니다.
Map<String, String> mdc = Mdc.copy();
if (!mdc.isEmpty()) sb.append(' ').append(mdc);
String startNanos = mdc.get("startNanos");
if (startNanos != null) {
long ms = (System.nanoTime() - Long.parseLong(startNanos)) / 1_000_000;
sb.append(" elapsed=").append(ms).append("ms");
}이렇게 하면 로그 한 줄만 보고도 요청이 시작된 이후 몇 밀리초가 지났는지 바로 알 수 있어, 느린 구간을 찾을 때 별도 계산 없이 눈으로 확인할 수 있습니다.
ArrayBlockingQueue 와 전용 스레드 하나로 파일에 쓰는 AsyncHandler 를 만드세요. 큐가 가득 차면 DEBUG 이하 레벨만 버리고, close() 에서는 큐에 남은 로그를 모두 쓴 뒤 종료하세요. 힌트: publish 는 큐에 넣기만 하고, 실제 쓰기는 별도 스레드의 무한 루프가 담당합니다.
public class AsyncHandler extends Handler {
private final BlockingQueue<LogRecord> queue = new ArrayBlockingQueue<>(1024);
private final Handler target;
private volatile boolean closed = false;
public AsyncHandler(Handler target) {
this.target = target;
Thread writer = new Thread(this::drain, "log-writer");
writer.setDaemon(true);
writer.start();
}
@Override
public void publish(LogRecord r) {
if (!queue.offer(r) && r.getLevel().intValue() > Level.FINE.intValue()) {
queue.poll(); // 가득 차면 오래된 DEBUG 급 로그만 버림
queue.offer(r);
}
}
private void drain() {
while (!closed || !queue.isEmpty()) {
LogRecord r = queue.poll();
if (r != null) target.publish(r);
}
}
@Override public void flush() { target.flush(); }
@Override public void close() { closed = true; target.close(); }
}offer 가 실패하면(큐 가득 참) 우선순위가 낮은 로그를 밀어내 중요한 로그의 유실을 줄이는 전략입니다. close() 는 closed 플래그만 세우고, 쓰기 스레드가 남은 큐를 다 비운 뒤 스스로 멈추도록 합니다.
로그 파일에서 같은 traceId 의 첫 줄과 마지막 줄 시각 차이를 awk 한 줄로 계산하세요. 힌트: traceId= 뒤 값을 키로 최초·최후 시각을 기록해 두고, 끝에서 차이를 출력합니다.
awk -F'traceId=' '/traceId=/{
split($2, a, "[,}]"); id = a[1]
if (!(id in first)) first[id] = $1
last[id] = $1
}
END {
for (id in first) print id, first[id], last[id]
}' app.logtraceId 별로 처음 등장한 줄과 마지막 등장한 줄의 시각 문자열을 나란히 출력해, 요청이 시작부터 끝까지 얼마나 걸렸는지 눈으로 비교할 수 있게 합니다. 정확한 밀리초 차이가 필요하면 시각 문자열을 초 단위로 변환하는 계산을 더 붙입니다.