1. 왜 로깅이 필요한가
로깅 ≠ 콘솔 출력
프로그래밍을 막 시작했을 때는 문제를 찾기 위해 여기저기에 System.out.println("여기 있었다!")를 넣기만 해도 충분해 보입니다. 이 방법은 프로그램이 작고 내 컴퓨터에서만 실행될 때는 통합니다. 하지만 대규모 프로젝트, 24/7로 동작하는 서버, 수천 명이 사용하는 애플리케이션을 상상해 보세요 — 모든 사용자의 콘솔을 옆에서 지켜볼 수는 없겠죠?
로깅은 단순히 메시지를 출력하는 것이 아닙니다. 애플리케이션 동작에 대한 정보(오류, 경고, 비즈니스 이벤트, 기술적 세부사항)를 체계적으로 기록하는 것입니다. 로그는 파일이나 데이터베이스에 저장되거나 네트워크로 전송될 수 있으며 — 실행이 끝난 뒤는 물론 실행 중에도 프로그램의 동작을 분석할 수 있게 해 줍니다.
로그는 왜 필요한가
- 문제 진단: 무언가 잘못되었을 때 로그는 가장 큰 도우미입니다. 어디서 왜 오류가 발생했는지 파악할 수 있습니다.
- 감사와 보안: 로그는 사용자의 중요한 행동을 기록하여 사고를 조사하는 데 도움이 됩니다.
- 디버깅: 어떤 버그는 특정 조건에서만 나타나며, 로그 없이는 잡기 어렵습니다.
- 모니터링: 로그로 애플리케이션의 상태, 성능, ‘건강’을 추적할 수 있습니다.
현실의 예
비행기에 “블랙박스”(비행 로그)가 없다면 사고 원인을 조사하기가 거의 불가능할 것입니다. 프로그래밍에서 로그는 여러분의 블랙박스입니다.
2. 로깅의 기본 레벨
로깅에서는 레벨(levels)을 사용합니다. 신호등과 같습니다: 빨강 — 위험, 노랑 — 주의, 초록 — 이상 없음.
주요 레벨(가장 ‘시끄러운’ 것부터 가장 ‘조용한’ 것까지):
| 레벨 | 설명 |
|---|---|
|
치명적 오류, 애플리케이션을 계속 실행할 수 없음 |
|
경고: 무언가 잘못되었지만 프로그램은 계속 동작 |
|
정상 동작에 대한 정보 메시지 |
|
디버깅을 위한 자세한 정보(주로 개발자에게만 표시) |
|
가장 상세한 정보 — 심층 진단용 |
예:
- 사용자가 비밀번호가 틀려 로그인하지 못함 — WARN.
- 데이터베이스가 고장남 — ERROR.
- 애플리케이션 시작 — INFO.
- 루프에서 변수 값을 출력 — DEBUG 또는 TRACE.
레벨 선택 방법
모든 것을 ERROR로 기록해서는 안 됩니다 — 그러면 진짜 오류를 놓치게 됩니다. 레벨을 의도적으로 사용하세요: 정말로 치명적인 장애만 오류로 표시하세요.
3. Java의 표준 로깅 (java.util.logging)
Java는 내장 로깅 시스템 — 패키지 java.util.logging(약칭 JUL) — 을 제공합니다. 별도 라이브러리를 추가하지 않아도 항상 사용할 수 있는 ‘기본’ 도구입니다.
주요 클래스:
- Logger — 로그를 기록하는 핵심 클래스.
- Level — 로깅 레벨 열거형(SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST).
- Handler — 로그를 어디에 쓸지 지정(파일, 콘솔 등).
- Formatter — 메시지 형식을 담당.
JUL 사용 예
import java.util.logging.Logger;
import java.util.logging.Level;
public class LoggingExample {
// 클래스 이름으로 로거 가져오기
private static final Logger logger = Logger.getLogger(LoggingExample.class.getName());
public static void main(String[] args) {
logger.info("애플리케이션이 시작되었습니다");
logger.warning("경고입니다!");
logger.severe("이건 오류입니다!");
int x = 42;
logger.fine("디버그 변수 x=" + x); // 기본 설정에서는 출력되지 않음
}
}
중요: 기본적으로 JUL은 INFO 이상의 레벨만 출력합니다. fine 및 기타 상세 메시지를 보려면 로깅 레벨을 설정해야 합니다.
파일을 통한 구성
JUL은 logging.properties 파일(보통 JRE 폴더에 있음)로 설정할 수 있습니다. 여기에서 다음을 지정할 수 있습니다:
- 로거의 최소 레벨
- 로그를 기록할 대상(파일, 콘솔)
- 메시지 포매팅
예시 설정 줄( logging.properties):
.level=INFO
4. 외부 로깅 라이브러리
Log4j (Apache)
Log4j는 Java에서 가장 잘 알려진 로깅 라이브러리 중 하나입니다. 유연하고 강력하며, 다양한 포맷, 비동기 기록, 파일 로테이션 등 많은 기능을 지원합니다.
Log4j 2의 가장 간단한 구성(XML) 예:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
코드에서 Log4j 2 사용 예:
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class Log4jExample {
private static final Logger logger = LogManager.getLogger(Log4jExample.class);
public static void main(String[] args) {
logger.info("Hello from Log4j!");
logger.error("Log4j를 통한 오류입니다");
}
}
작동하려면 Log4j 의존성을 프로젝트에 추가해야 합니다(예: Maven 또는 Gradle).
SLF4J: 로깅 파사드
SLF4J(Simple Logging Facade for Java)는 독립적인 로깅 시스템이 아니라, 코드와 구체적 구현(Log4j, Logback, JUL 등) 사이의 ‘얇은 레이어’입니다.
파사드를 사용하면 코드를 한 번만 작성하고, 서로 다른 라이브러리가 다른 로거를 사용하더라도 코드를 고치지 않고 실제 로깅 시스템을 교체할 수 있습니다.
SLF4J 사용 예:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Slf4jExample {
private static final Logger logger = LoggerFactory.getLogger(Slf4jExample.class);
public static void main(String[] args) {
logger.info("안녕하세요, SLF4J로 로깅합니다!");
logger.warn("주의: 문제가 생길 수 있습니다");
logger.error("SLF4J를 통한 오류");
}
}
동작 방식은?
- 코드는 SLF4J API로 작성합니다.
- 클래스패스에 원하는 구현체(예: Logback 또는 Log4j)를 추가합니다.
- SLF4J가 호출을 선택한 구현체로 전달합니다.
SLF4J와 다른 로거의 연동: SLF4J는 JUL, Log4j, Logback 등 위에서 동작할 수 있어 코드를 다시 쓰지 않고도 손쉽게 마이그레이션할 수 있습니다.
5. 실습: System.out.println과 로깅 비교
예제 1: System.out.println
public class PrintlnExample {
public static void main(String[] args) {
System.out.println("애플리케이션이 시작되었습니다");
System.out.println("오류 발생: 무언가 잘못되었습니다");
}
}
문제점:
- 메시지 레벨이 없음(오류, 정보, 경고 — 모두 동일).
- 시간, 클래스 이름, 스레드 정보가 없음.
- 모든 메시지가 한데 섞임.
- 유연한 출력 제어 불가(예: 오류만 출력).
예제 2: SLF4J + Logback 로깅
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LoggingVsPrintln {
private static final Logger logger = LoggerFactory.getLogger(LoggingVsPrintln.class);
public static void main(String[] args) {
logger.info("애플리케이션이 시작되었습니다");
logger.error("오류 발생: 무언가 잘못되었습니다");
}
}
장점:
- 각 메시지에 시간, 레벨, 클래스 이름, 스레드가 함께 기록됩니다.
- 레벨별로 메시지를 필터링할 수 있습니다.
- 파일 기록, 네트워크 전송, 포맷팅, 아카이빙 등을 할 수 있습니다.
- 로깅은 스레드 안전합니다(멀티스레드 애플리케이션에 중요).
차이 시연
|
로깅(SLF4J/Logback) |
|---|---|
| 애플리케이션이 시작되었습니다 | 12:34:56 [main] INFO LoggingVsPrintln - 애플리케이션이 시작되었습니다 |
| 오류 발생: 무언가 잘못되었습니다 | 12:34:56 [main] ERROR LoggingVsPrintln - 오류 발생: 무언가 잘못되었습니다 |
6. 프로젝트에 로깅 추가하기
표준 로거(JUL) 사용 시
JDK에 이미 포함되어 있으므로 바로 사용할 수 있습니다:
import java.util.logging.Logger;
public class MyApp {
private static final Logger logger = Logger.getLogger(MyApp.class.getName());
public static void main(String[] args) {
logger.info("정보 메시지입니다");
}
}
Log4j 또는 SLF4J 사용 시
- 의존성을 추가 (예: Maven).
Log4j 2:
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.20.0</version> </dependency>SLF4J + Logback:
<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>2.0.7</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.4.7</version> </dependency> - 구성 파일을 생성 (예: Logback은 logback.xml, Log4j는 log4j2.xml).
- 코드에서 로거를 사용 (위 예시 참고).
7. 미니 애플리케이션: 로깅 추가
간단한 커맨드라인 계산기 애플리케이션이 있다고 가정해 봅시다. 여기에 로깅을 추가해 보겠습니다.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Scanner;
public class CalculatorApp {
private static final Logger logger = LoggerFactory.getLogger(CalculatorApp.class);
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
logger.info("계산기가 시작되었습니다");
try {
System.out.print("첫 번째 숫자를 입력하세요: ");
int a = Integer.parseInt(scanner.nextLine());
logger.debug("입력된 첫 번째 숫자: {}", a);
System.out.print("두 번째 숫자를 입력하세요: ");
int b = Integer.parseInt(scanner.nextLine());
logger.debug("입력된 두 번째 숫자: {}", b);
int sum = a + b;
logger.info("합계: {}", sum);
System.out.println("결과: " + sum);
} catch (NumberFormatException ex) {
logger.error("숫자 입력 오류", ex);
System.out.println("오류: 올바른 숫자를 입력하세요!");
} catch (Exception ex) {
logger.error("알 수 없는 오류", ex);
System.out.println("오류가 발생했습니다!");
}
logger.info("계산기 종료");
}
}
여기서 무엇을 했나:
- 모든 핵심 이벤트를 로그에 기록합니다.
- 오류는 ERROR 레벨과 전체 스택 트레이스로 기록합니다.
- 프로그램 실행 후에도 분석할 수 있도록 모든 정보가 남습니다.
8. 로깅에서 흔한 실수
오류 №1: 디버깅과 진단에 System.out.println 만 사용하는 것.
초기에는 편하지만 실서비스 애플리케이션에는 맞지 않습니다. 메시지 레벨을 제어할 수 없고, 시간 정보도 없으며, 프로그램이 서버에서 동작할 때 로그를 분석하기도 어렵습니다.
오류 №2: 너무 많은 정보를 ERROR 레벨로 기록.
모든 것을 오류로 기록하면 정말 중요한 것과 단지 디버깅 정보가 무엇인지 구분하지 못하게 됩니다.
오류 №3: 민감한 데이터 기록 (비밀번호, 토큰, 카드 번호).
로그는 안전해야 합니다! 남이 보면 안 되는 것을 기록하지 마세요.
오류 №4: 로깅 구성 부재.
레벨, 포맷, 저장 위치를 설정하지 않으면 — 원치 않는 방대한 로그에 ‘파묻히거나’ 아예 오류를 놓칠 수 있습니다.
오류 №5: 대형 프로젝트에서 로깅 파사드 (SLF4J) 를 사용하지 않는 것.
프로젝트가 커지면서 한 라이브러리에서 다른 라이브러리로 옮길 때 — 파사드가 없으면 매우 고통스럽습니다. SLF4J는 코드를 고치지 않고도 로깅 ‘엔진’을 쉽게 바꿀 수 있게 해 줍니다.
GO TO FULL VERSION