공공부하자개발 · 영어 학습 노트
자바
실무 확장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정리‹ 이전다음 ›

3. 코드 예제

실행 방법입니다. 외부 jar 는 필요 없습니다.

powershell
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/ 폴더 생성

예제 1: 레벨·계층과 유효 레벨

java
// 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("재고 임계치 도달");
text
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() 는 조상을 따라 올라가며 레벨이 있는 첫 로거를 찾는 함수입니다.

예제 2: MDC 요청별 격리

java
// 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();   // 스레드 풀 재사용 시 이전 요청 값 오염 방지
    }
}
text
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() 가 건너뛰어져 다음 요청에 값이 남습니다.

예제 3: MDC 스레드 전파

java
// 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); }
    };
}
text
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 메서드마다 이 복사를 자동으로 해 줍니다.

예제 4: 예외 로깅 두 방식

java
// Main.exceptionLogging
try {
    Integer.parseInt("12a");
} catch (NumberFormatException e) {
    SVC.severe("파싱 실패: " + e.getMessage() + "   ← 어디서 났는지 모름");
    SVC.log(Level.SEVERE, "파싱 실패", e);   // Logback: log.error("파싱 실패", e)
}
text
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번 중복으로 남습니다.

예제 5: 마스킹 필터

java
// 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;
}
text
10:52:18.761 INFO    [main] app.service - 회원 가입 rrn=901231-******* card=1234-****-****-3456 phone=010-****-5678 mail=ho***@test.com

isLoggable 안에서 setMessage 로 원본 메시지를 마스킹한 버전으로 바꿔치기하고, 항상 true 를 반환해 로그 자체는 통과시킵니다. 이 필터는 콘솔·파일 등 어떤 핸들러 앞에도 붙일 수 있어 마스킹 로직을 한곳에 모을 수 있습니다.

Formatters.Line.format — 한 줄 포맷의 뼈대

java
// 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 가 없는 일반 로그의 가독성을 해치지 않습니다.

예제 직접 실행

아래 폴더를 JDK 21 로 컴파일하고 실행합니다. 외부 jar 를 쓰는 레슨은 java-src/lib 를 클래스패스에 넣습니다.

cd java-src\extension\10_logging
javac -encoding UTF-8 *.java && java Main

:: 외부 jar 가 필요한 레슨
javac -encoding UTF-8 -cp "..\..\lib\*;." *.java && java -cp "..\..\lib\*;." Main
코드 예제
  • 예제 1: 레벨·계층과 유효 레벨
  • 예제 2: MDC 요청별 격리
  • 예제 3: MDC 스레드 전파
  • 예제 4: 예외 로깅 두 방식
  • 예제 5: 마스킹 필터
  • Formatters.Line.format — 한 줄 포맷의 뼈대
이전 섹션2 핵심 원리3 / 7다음 섹션4 응용 변형 예제