공공부하자개발 · 영어 학습 노트
자바
실무 확장Excel · 파일 업로드 · DB 연동0/22 완료
  • 01Excel(XLSX) 구조와 순수 JDK로 읽기/쓰기
  • 02Apache POI로 Excel 업로드/다운로드
  • 03파일 업로드/다운로드 서버 (HttpServer)
  • 04JDBC 기초와 트랜잭션 (H2)
  • 05MyBatis 어노테이션 매퍼로 쿼리 연동
  • 06MyBatis XML 매퍼 · Oracle 방언 · PageHelper · Spring Boot
  • 07REST API 서버와 JSON
  • 08Vue 3 SPA 와 Java 서버 연동
  • 09@Scheduled 운영
  • 10로깅 실무: 레벨·계층, MDC 추적, 예외·성능, 마스킹, 롤링, JSON 로그
  • 11외부 API 연동
  • 12테스트 실무
  • 13암호화·개인정보 보호
  • 14인코딩·한글 실무
  • 15@Transactional 심화
  • 16긴 작업 비동기 처리와 진행률
  • 17SFTP·FTP 파일 연계
  • 18로컬 캐시와 @Cacheable
  • 19메일·알림 발송
  • 20웹 보안 체크리스트
  • 21빌드 도구와 폐쇄망 의존성 반입
  • 22성능 측정: p50·p95·p99, 측정 계층, JFR, JMH 함정, 자체 부하 테스트, 병목 순위
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 실무 확장 › 10 / 22

로깅 실무: 레벨·계층, MDC 추적, 예외·성능, 마스킹, 롤링, JSON 로그

섹션 7진행 0 / 22
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

6. 연습 문제

이 레슨의 네 파일에 직접 코드를 덧붙이는 문제 5개입니다. 정답을 보기 전에 먼저 힌트만 보고 스스로 작성해 보세요.

문제 1. Mdc 에 try-with-resources 로 쓸 수 있는 스코프 추가

Mdc 에 AutoCloseable 을 반환하는 scope(key, value) 를 추가해, try (var s = Mdc.scope("traceId", id)) { ... } 형태로 쓰면 블록을 나갈 때 자동으로 해당 키를 지우도록 하세요. 힌트: close() 안에서 remove(key) 를 호출하는 익명 클래스나 람다를 반환합니다.

java
public static AutoCloseable scope(String key, String value) {
    put(key, value);
    return () -> remove(key);
}

finally { Mdc.clear(); } 를 매번 손으로 쓰는 대신, try 선언부에 넣기만 하면 됩니다. 여러 키를 동시에 여닫아야 하면 scope 를 여러 번 중첩해서 씁니다.

문제 2. MaskingFilter 에 Authorization 토큰 마스킹 추가

MaskingFilter 에 Authorization: Bearer xxx 형태의 토큰을 마스킹하는 패턴을 추가하고, 토큰의 앞 6자만 남기도록 하세요. 힌트: Bearer 뒤 문자열을 그룹으로 잡고 앞부분만 남기는 치환을 씁니다.

java
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*** 가 나와야 정답입니다.

인증 토큰은 그대로 노출되면 세션 탈취로 이어질 수 있어 반드시 마스킹 대상입니다.

문제 3. Formatters.Line 에 소요 시간 필드 추가

요청 시작에 Mdc.put("startNanos", String.valueOf(System.nanoTime())) 을 넣어 두고, Formatters.Line.format 이 현재 시각과의 차이를 밀리초로 계산해 줄 끝에 덧붙이도록 하세요. 힌트: MDC 값은 문자열이므로 Long.parseLong 으로 바꿔 계산합니다.

java
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");
}

이렇게 하면 로그 한 줄만 보고도 요청이 시작된 이후 몇 밀리초가 지났는지 바로 알 수 있어, 느린 구간을 찾을 때 별도 계산 없이 눈으로 확인할 수 있습니다.

문제 4. 큐 기반 비동기 파일 핸들러 만들기

ArrayBlockingQueue 와 전용 스레드 하나로 파일에 쓰는 AsyncHandler 를 만드세요. 큐가 가득 차면 DEBUG 이하 레벨만 버리고, close() 에서는 큐에 남은 로그를 모두 쓴 뒤 종료하세요. 힌트: publish 는 큐에 넣기만 하고, 실제 쓰기는 별도 스레드의 무한 루프가 담당합니다.

java
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 플래그만 세우고, 쓰기 스레드가 남은 큐를 다 비운 뒤 스스로 멈추도록 합니다.

문제 5. traceId 별 요청 소요 시간 계산

로그 파일에서 같은 traceId 의 첫 줄과 마지막 줄 시각 차이를 awk 한 줄로 계산하세요. 힌트: traceId= 뒤 값을 키로 최초·최후 시각을 기록해 두고, 끝에서 차이를 출력합니다.

bash
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.log

traceId 별로 처음 등장한 줄과 마지막 등장한 줄의 시각 문자열을 나란히 출력해, 요청이 시작부터 끝까지 얼마나 걸렸는지 눈으로 비교할 수 있게 합니다. 정확한 밀리초 차이가 필요하면 시각 문자열을 초 단위로 변환하는 계산을 더 붙입니다.

연습 문제
    이전 섹션5 자주 하는 실수 (Tip)6 / 7다음 섹션7 정리