본문 바로가기
개발자 기초 & 실무

콘솔 로그로 디버깅하기 — println에서 logback까지 실무 가이드

by yamoojin83 2025. 11. 13.

콘솔 로그로 디버깅하기 — println에서 logback까지 실무 가이드

개발자는 결국 ‘문제 해결가’입니다.

그리고 문제를 해결하기 위한 가장 첫 번째 도구가 바로 ‘로그(Log)’입니다.

이번 글에서는 초보 개발자들이 가장 자주 사용하는 System.out.println()부터 실무에서 표준처럼 쓰이는 SLF4J + Logback까지, 디버깅의 기본기를 단계별로 정리하고 확장 사례까지 자세히 다룹니다.

java 콘솔 로그 디버깅 가이드

java 콘솔 로그 디버깅 가이드

1. println으로 시작하는 디버깅의 기본

처음 자바를 배우면 대부분 System.out.println()으로 코드를 점검합니다.

간단하고 즉각적인 확인이 가능하지만, 규모가 커지면 로그가 섞이고 찾기 어려워집니다.


public void saveUser(User user) {
    System.out.println("사용자 저장 시작");
    userRepository.save(user);
    System.out.println("저장 완료: " + user.getName());
}

💡 장점: 빠르고 간단하다.

⚠️ 단점:
- 콘솔에만 출력되어 기록이 남지 않음
- 레벨(중요도) 구분 불가 → 운영 시 소음(Noise) 심함
- 배포 환경에서 불필요한 출력은 성능 저하·보안 리스크가 될 수 있음

2. Logger 도입 — SLF4J + Logback

println의 한계를 보완하기 위해 로깅 프레임워크를 사용합니다.

자바 세계에서는 SLF4J(로깅 추상화) 위에 Logback(구현)을 많이 씁니다. Spring Boot 기본 스택이기도 해서 도입 난이도가 낮습니다.


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class UserService {
    private static final Logger log = LoggerFactory.getLogger(UserService.class);

    public void saveUser(User user) {
        log.info("사용자 저장 시작");
        userRepository.save(user);
        log.info("저장 완료: {}", user.getName());
    }
}

💡 장점 요약:
- DEBUG/INFO/WARN/ERROR 레벨별 출력 제어
- 콘솔/파일/원격 등 다양한 Appender로 기록 보존 가능
- JSON 포맷, MDC(요청 트레이싱) 등 실무 기능 확장 용이

 

3. 로그 레벨(Level)의 이해

로그 레벨은 메시지의 중요도를 구분합니다. 환경별로 다르게 출력하도록 설정합니다.


log.trace("가장 상세한 단계");
log.debug("개발자 디버깅용 메시지");
log.info("정상 동작 알림");
log.warn("주의가 필요한 상황");
log.error("에러 발생!");

💡 환경 규칙 추천:
- dev: DEBUG 이상 출력
- stage: INFO 이상
- prod: WARN 이상 (필요 시 ERROR만)

4. logback-spring.xml 최소 설정

Spring Boot 기준 src/main/resources/logback-spring.xml에 배치합니다.


<configuration>
  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>[%d{HH:mm:ss.SSS}] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <root level="info">
    <appender-ref ref="CONSOLE"/>
  </root>
</configuration>

이렇게 하면 [시:분:초.밀리초] 레벨 로거명 - 메시지 형태로 출력됩니다.

팀 내 스타일을 통일하면 검색·분석 속도가 크게 개선됩니다.

5. 실무 디버깅 절차 — 로그로 문제 찾기

로그는 “발생 시점 → 원인 경로”를 따라 역추적할 때 빛을 발합니다. 다음 순서를 습관화하세요.

  1. 에러가 난 시각의 ERROR 로그를 먼저 찾는다.
  2. 바로 앞선 WARN/DEBUG를 연쇄적으로 추적한다.
  3. StackTrace에서 내 코드(패키지) 경로만 필터링해 핵심 지점을 본다.

💡 팁: 변수를 log.debug("id={}, size={}", id, list.size())처럼 구조적으로 남기면 디버거 없이도 상태를 재구성할 수 있습니다.

6. println의 한계 ‘실험’ — 성능과 동시성

“println도 되는데 굳이 Logger?”라는 질문에 실험으로 답해봅니다.

아래 코드는 1000회 반복 출력멀티스레드 동시 출력 상황을 단순 재현합니다.


// 1000회 반복 출력
for (int i = 0; i < 1000; i++) {
    System.out.println("P " + i);
    // log.debug("L {}", i); // 대조군
}

// 멀티스레드 동시 출력 (순서 뒤섞임 관찰)
ExecutorService es = Executors.newFixedThreadPool(8);
for (int t = 0; t < 8; t++) {
    es.submit(() -> {
        for (int i = 0; i < 200; i++) {
            System.out.println(Thread.currentThread().getName() + " P " + i);
            // log.debug("{}", i);
        }
    });
}
es.shutdown();

관찰 포인트:
- println은 버퍼링·비동기·레벨 제어가 없어 콘솔 병목이 쉽습니다.
- 스레드가 많아지면 출력 순서가 뒤섞여 원인 파악이 어려워집니다.
- 운영 환경에서는 파일 회전/보관, 레벨 필터링이 필수인데 println로는 불가합니다.

결론: 학습·소규모 테스트에는 println이 빠르지만, 실무·운영은 Logback을 써야 합니다.

7. 파일로 남기기 — RollingFileAppender (일자·용량 분할)

운영 환경에서는 파일 보관회전(로테이션)이 핵심입니다.

일자 기준(또는 용량 기준)으로 파일을 분할·보존하는 설정 예시는 다음과 같습니다.


<configuration>
  <property name="LOG_DIR" value="./logs"/>

  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${LOG_DIR}/app.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>${LOG_DIR}/app.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
      <maxHistory>30</maxHistory>  <!-- 30일 보관 -->
    </rollingPolicy>
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <logger name="com.example" level="debug"/>

  <root level="info">
    <appender-ref ref="CONSOLE"/>
    <appender-ref ref="FILE"/>
  </root>
</configuration>

설명:
- TimeBasedRollingPolicy로 일자별 파일 분할, 자동 압축(.gz)
- maxHistory로 보관 기간 설정(예: 30일)
- 콘솔과 파일에 동시에 기록해 개발/운영 분석 모두 대비

8. 실전 로그 디버깅 ‘사례’ — 500 에러 2분 컷

증상: /api/orders 호출 시 500 발생, 프론트는 “결제 실패”만 표시됨.

접근: 요청 트레이스 ID로 요청 단위를 묶어 추적(MDC 사용 시 효과 극대화).


// 요청 시작 시
log.info("[traceId={}] 주문 생성 시작 userId={}", traceId, userId);

// 서비스 내부
log.debug("[traceId={}] 재고 검증 itemId={} qty={}", traceId, itemId, qty);

// 예외 지점
try {
    paymentClient.charge(...);
} catch (PaymentException e) {
    log.error("[traceId={}] 결제 실패 code={} msg={}", traceId, e.getCode(), e.getMessage(), e);
    throw e;
}

해결: 로그에서 PaymentException code=PG-401 확인 → 결제 키 만료. 운영 키 재발급 후 정상화.

이처럼 맥락 있는 필드(traceId, userId, itemId, code 등)를 구조적으로 남기면 원인 추적에 걸리는 시간이 극적으로 줄어듭니다.

9. 로그 ‘메시지 설계’ 팁 — 읽히는 로그가 답이다

- 한 줄에 누가(주체), 언제(시각), 무엇을(행위), 어떻게 됐는지(결과/수치) 포함
- 불필요한 리스트/대용량 객체는 요약(크기·식별자만)
- 개인정보/비밀키는 마스킹 또는 출력 금지
- log.info("결제 성공 id={}, amount={}", paymentId, amount) 처럼 키=값 패턴 유지

 

java 로그 디버깅 완성 화면

java 로그 디버깅 완성 화면

마무리

System.out.println()은 학습·소규모 테스트에 유용하지만, 실무와 운영 환경에서는 SLF4J + Logback이 정답입니다.

레벨·포맷·파일보관·트레이싱을 갖춘 로그는 디버깅 속도를 높이고 팀의 문제해결력을 끌어올립니다. 오늘부터 로그를 “데이터”로 다뤄보세요.

다음 글에서는 “크롬 개발자도구(DevTools) 완전 활용법 — 네트워크, 콘솔, 디버거까지”를 다룹니다.

백엔드 로그와 프론트 DevTools를 연결하면, 문제는 더 쉽게 보입니다.