1. 로그 메시지의 구조
로그는 단순한 “의식의 흐름”이 아니라, 한 달 혹은 1년 뒤에 여러분이나 동료가 “대체 여기서 무슨 일이 벌어졌지?”라는 질문에 답을 찾을 수 있는 소중한 기록입니다. 이를 가능하게 하려면 각 로그 메시지는 구조화되어야 합니다. 보통(대부분의 라이브러리에서 표준) 각 메시지는 다음을 포함합니다:
- 이벤트 시간 — 언제 발생했는지.
- 레벨 — 얼마나 중요한지(INFO, ERROR 등).
- 로거 이름 — 보통 클래스나 컴포넌트의 이름.
- 메시지 텍스트 — 무엇이 일어났는지.
- 스택 트레이스(오류가 있는 경우) — 어디서 왜 발생했는지 이해하기 위함.
다음은 잘 포맷팅된 로그 한 줄의 예입니다(Log4j/SLF4J 기준):
2024-06-16 18:42:07,123 INFO com.example.MainApp - 사용자가 시스템에 로그인함: username=vasya
오류가 발생한 경우:
2024-06-16 18:42:10,456 ERROR com.example.LoginService - 사용자 인증 오류: vasya
java.lang.IllegalArgumentException: 잘못된 비밀번호
at com.example.LoginService.checkPassword(LoginService.java:42)
...
왜 중요할까요?
애플리케이션이 오래 실행되면 로그는 기가바이트 단위로 쌓일 수 있습니다. 메시지가 구조화되어 있지 않다면, 문제를 찾는 일은 “선풍기 소음만 듣고 멜로디를 맞히는 수준”의 난이도가 됩니다.
2. 메시지 포맷팅
이렇게 하면 안 됩니다:
logger.info("사용자 " + username + "가 시스템에 로그인했습니다");
겉보기에는 간단하지만 함정이 있습니다. 로깅 레벨이 현재 ERROR로 설정되어 있어도, 괄호 안의 문자열은 여전히 조합됩니다(문자열 연결이 수행됨). 이는 불필요한 리소스 낭비입니다. 초당 수천 줄을 기록하는 대규모 시스템에서는 실제 지연으로 이어질 수 있습니다.
올바른 방법: 템플릿과 파라미터
현대 라이브러리(SLF4J, Log4j 2 등)는 파라미터가 있는 템플릿을 지원합니다:
logger.info("사용자 {}가 시스템에 로그인했습니다", username);
이 경우 해당 메시지를 출력할 수 있는 로깅 레벨일 때만 문자열이 조합됩니다. 예를 들어 현재 WARN이라면 문자열 계산조차 하지 않습니다 — 리소스와 시간을 절약합니다.
보너스: 여러 파라미터를 전달하면 순서대로 치환됩니다:
logger.info("사용자 {}가 {} 작업을 객체 {}에서 수행했습니다", username, action, objectId);
예외 로깅(stacktrace)
예외를 캐치했다면 스택 트레이스를 수동으로 메시지에 붙이지 마세요:
// 하지 마세요:
logger.error("오류: " + ex.getMessage() + "\n" + Arrays.toString(ex.getStackTrace()));
올바른 방법:
logger.error("요청 처리 중 오류", ex);
SLF4J와 Log4j는 스택 트레이스를 로그에 알아서 보기 좋게 추가합니다.
예시: 접근 방식 비교
// 나쁨(문자열 연결은 항상 수행됨)
logger.debug("객체: " + expensiveToString(obj));
// 좋음(지연 형성)
logger.debug("객체: {}", obj);
3. 로깅 레벨 선택
모든 것을 ERROR 레벨로만 찍는다면, 그것은 더 이상 로그가 아니라 “빨간 경고등”입니다. 반대로 전부 DEBUG라면 디테일에 파묻히게 됩니다. 각 레벨을 언제 사용할지 정리해 봅시다.
| 레벨 | 사용 목적 | 메시지 예시 |
|---|---|---|
|
시스템이 오작동하거나 아예 동작하지 않게 만드는 치명적 장애 | “데이터베이스 연결 오류” |
|
치명적이지 않지만 주의가 필요한 중요한 경고 | “사용자를 찾지 못했습니다, guest를 사용합니다” |
|
애플리케이션의 정상 동작을 나타내는 일반 이벤트 | “사용자 등록됨: vasya” |
|
디버깅을 위한 상세 정보, 프로덕션에서는 불필요 | “메서드 checkPassword 호출됨, 파라미터: ...” |
|
가장 상세한 정보, 보통 심층 진단용 | “처리 루프 시작: i=0” |
대표적인 메시지 예
- ERROR — 파일 기록 실패, 처리되지 않은 예외 포착, 서비스 불가.
- WARN — 사용 중단 예정 API, 의심스러운 사용자 행동, 시도 횟수 제한 초과.
- INFO — 사용자 로그인/로그아웃, 주문 처리 완료, 애플리케이션 시작.
- DEBUG — 요청 파라미터, 변수 값, 중간 계산 결과.
- TRACE — 메서드 진입/퇴출, 내부 루프, 알고리즘 동작 상세.
팁:
프로덕션에서는 보통 INFO 이상만 활성화하며, 경우에 따라 WARN과 ERROR만 둡니다. DEBUG와 TRACE는 복잡한 버그를 추적할 때만 켭니다.
4. Best practices (로깅 모범 사례)
민감한 데이터를 로깅하지 마세요
비밀번호, 토큰, 신용카드 번호 — 이런 것들은 로그에 있어서는 안 됩니다. “로그 파일은 나만 본다”라고 생각할지 몰라도, GDPR을 기억하고, 누군가가 로그를 실수로 공용 채팅에 올릴 수도 있음을 떠올리세요.
// 나쁨:
logger.info("사용자 {}가 비밀번호 {}로 로그인했습니다", username, password);
// 좋음:
logger.info("사용자 {}가 시스템에 로그인했습니다", username);
ERROR 레벨을 남발하지 마세요
모든 것을 logger.error로 찍어 버리면, 진짜 재난이 발생했을 때 아무도 알아차리지 못합니다 — 모두 “빨간 불”에 익숙해져 버렸거든요. 애플리케이션이 실제로 작업을 계속할 수 없거나 비즈니스 로직이 깨진 상황에만 ERROR를 사용하세요.
예외는 전체 스택과 함께 로깅하세요
ex.getMessage()만 찍지 마세요. 그러면 어디서 오류가 발생했는지 결코 알 수 없습니다. 예외를 두 번째 파라미터로 로거에 전달하세요.
logger.error("요청 처리 중 오류", ex);
고유 식별자 사용(이벤트 상관관계)
대규모 시스템에서는 각 요청, 사용자 또는 작업에 고유 식별자를 부여하면 유용합니다. 시스템의 다양한 부분에서 발생한 이벤트를 “엮어” 볼 수 있습니다.
logger.info("주문 처리 시작: orderId={}", orderId);
logger.info("주문 처리 성공: orderId={}", orderId);
무분별하게 모든 것을 로깅하지 마세요
로그가 너무 많아지면 쓸모가 없어집니다. 코드 한 줄 한 줄을 로깅하지 마세요. 그러면 필요한 정보를 찾기 어려워집니다.
메시지를 이해하기 쉽게 포맷팅하세요
코드 작성자뿐만 아니라 6개월 뒤에 로그를 읽을 사람도 이해할 수 있도록 작성하세요. 약어, 불명확한 축약, 내부자 농담은 피하세요.
5. 실습: 로그 포맷과 레벨 설정
Log4j2 설정 예시(log4j2.xml)
<Configuration>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
무엇을 의미하나요?
- %d{...} — 이벤트 시간.
- %-5level — 레벨(ERROR, INFO 등).
- %logger{36} — 로거 이름(보통 클래스).
- %msg — 메시지 본문.
서로 다른 로깅 레벨의 코드 예시(SLF4J)
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogDemo {
private static final Logger logger = LoggerFactory.getLogger(LogDemo.class);
public static void main(String[] args) {
logger.info("애플리케이션이 시작되었습니다");
logger.debug("변수 x의 값: {}", 42);
try {
throw new IllegalArgumentException("아이 아이 아이!");
// ...
} catch (Exception ex) {
logger.error("시작 중 오류가 발생했습니다", ex);
}
}
}
레벨 간 차이 데모
로거 설정에서 INFO로 지정되어 있으면 DEBUG 이하 레벨의 메시지는 출력되지 않습니다. 설정에서 레벨을 debug로 바꿔 보세요 — 더 자세한 로그를 볼 수 있습니다.
6. 자주 하는 실수
오류 №1: 로그에서 문자열 연결. 초보자는 아주 흔히 이렇게 작성합니다:
logger.debug("사용자: " + user.getName() + ", 역할: " + user.getRole());
그 결과 DEBUG 레벨이 꺼져 있어도 이 문자열은 조합됩니다. 이는 불필요한 부담을 야기합니다. 파라미터를 사용하세요!
오류 №2: 스택 없는 예외 로깅. 메시지만 작성합니다:
logger.error("오류: " + ex.getMessage());
결국 로그에는 어디서 오류가 발생했는지 정보가 없습니다. 예외를 두 번째 파라미터로 전달하세요!
오류 №3: 모든 것을 ERROR 레벨로 로깅. 전부 빨갛다면 아무것도 빨갛지 않은 것과 같습니다. 레벨을 용도에 맞게 사용하지 않으면, 중요한 오류가 “사소한 것들” 속에 묻혀 버립니다.
오류 №4: 민감한 데이터 로깅. 비밀번호, 토큰, 카드 번호는 절대 로그에 기록하지 마세요. 아무도 못 볼 것 같아도, 인생은 서프라이즈가 많습니다.
오류 №5: 이해하기 어려운 메시지. 로그 메시지가 “ERR42: fail”처럼 보인다면, 한 달 뒤에 여러분 자신도 무슨 뜻인지 기억하지 못할 겁니다. 명확하고 자세하게 작성하세요.
오류 №6: 고유 식별자 부재. 복잡한 시스템에서 orderId, userId 같은 식별자가 없으면 이벤트를 “엮어” 특정 사용자나 주문에 무슨 일이 있었는지 파악할 수 없습니다.
GO TO FULL VERSION