초보 개발자와 실무 개발자를 구분하는 가장 확실한 기준 하나를 꼽으라면 바로 '로깅(Logging)을 어떻게 다루는가?'입니다.
대학생이나 학원 시절 프로젝트에서는 에러가 나거나 변수 값을 확인하고 싶을 때 무조건 System.out.println("바로여기")를 쓰곤 했었는데요. 하지만 실제 운영(Production) 서버에서 System.out.println을 쓰면, 치명적인 성능 저하(블로킹 I/O)가 발생할 뿐만 아니라, 서버가 재시작되는 순간 콘솔에 찍힌 글자들이 흔적도 없이 증발해 버립니다.
어젯밤 11시에 결제가 실패해서 고객센터로 항의가 들어왔는데, 원인을 찾으려고 보니 로그 기록이 완전히 날아가 있다면 개발자는 등골이 오싹해질 수밖에 없습니다.
이번 글에서는 장애를 역추적하는 Spring Boot의 기본 로깅 프레임워크인 Logback(로그백)의 완벽한 통제와 파일 로테이션(Rotation) 실무 기법에 대하여 다뤄보겠습니다.
1. 왜 System.out.println을 쓰면 안될까?
자바의 System.out.println()은 매우 무거운 동기(Synchronous) 작업입니다. 한 쓰레드가 화면에 글자를 다 찍을 때까지 다른 쓰레드들은 줄을 서서 기다려야 합니다. 또한, 이 텍스트들을 레벨(Level)별로 분류할 수도 없고, 파일로 저장할 수도 없습니다.
반면 전문 로깅 프레임워크(Slf4j + Logback)를 사용하면 TRACE < DEBUG < INFO < WARN < ERROR 라는 의 로깅 레벨을 부여할 수 있습니다. "개발 서버에서는 아주 사소한 변수 값(DEBUG)까지 다 출력하고, 상용 서버에서는 성능을 위해 중요한 에러(ERROR)나 알림(INFO)만 출력해라!"라는 섬세한 튜닝이 yml 파일 수정 한 줄로 가능해집니다.
2. Slf4j와 Lombok @Slf4j의 우아한 활용 (중요⭐)
스프링 부트(Spring Boot)는 내부에 Logback이라는 빠르고 강력한 로깅 엔진을 기본 탑재하고 있습니다. 개발자는 이 엔진을 직접 호출하는 대신, Slf4j(Simple Logging Facade for Java)라는 공통 인터페이스를 통해 로그를 남깁니다. 롬복(Lombok)을 쓰고 있다면 클래스 위에 @Slf4j 어노테이션 하나만 붙이면 log 객체가 마법처럼 생성됩니다.
@Slf4j // Lombok이 자동으로 private static final Logger log = LoggerFactory.getLogger(...) 코드를 만들어줍니다!
@Service
public class OrderService {
public void processOrder(Long orderId) {
log.info("주문 처리 시작. Order ID: {}", orderId); // {} 안에 변수가 쏙 들어갑니다 (성능 최적화
try {
// 결제 로직...
log.debug("세부 결제 데이터 검증 중..."); // 개발 서버에서만 보고 싶은 상세 로그
} catch (Exception e) {
log.error("주문 처리 중 치명적인 에러 발생! Order ID: {}", orderId, e); // 에러 로그와 Exception 객체를 함께 넘김
}
}
}
3. logback-spring.xml: 파일 기반 영구 저장과 로테이션(Rotation)
콘솔에 로그를 예쁘게 찍는 것까지는 성공했습니다. 이제 진짜 실무의 영역인 '로그 파일 남기기' 차례입니다. 로그를 한 파일(예: error.log)에 1년 내내 누적해서 쓴다면 나중에는 파일 용량이 100GB가 넘어가서 메모장으로 열 수도 없게 됩니다.
실무에서는 "매일 자정(밤 12시)이 되면 어제 썼던 로그 파일을 날짜를 붙여서(예: error-2024-03-15.log) 압축(zip) 보관하고, 30일이 지난 아주 오래된 로그는 서버 용량을 위해 자동으로 싹 지워라!"라는 마법의 룰을 적용합니다. 이를 RollingFileAppender (롤링 파일 어펜더)라고 부릅니다. src/main/resources 아래에 logback-spring.xml 파일을 만들어 이 완벽한 룰을 지시합니다.
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 로그 파일이 저장될 폴더 위치 -->
<property name="LOG_PATH" value="/var/log/my-awesome-app"/>
<!-- 1. 날짜별로 쪼개서(Rolling) 저장하는 로직 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/application.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 매일 자정마다 새로운 파일로 분리하고, 지난 파일은 날짜를 붙여 압축(.gz) -->
<fileNamePattern>${LOG_PATH}/application-%d{yyyy-MM-dd}.log.gz</fileNamePattern>
<!-- 과거 로그는 최대 30일(한 달) 치만 보관하고 나머지는 자동 삭제! (디스크 터짐 방지) -->
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<!-- 로그에 찍힐 포맷: [날짜/시간] [쓰레드명] [로그레벨] [클래스명] - 로그내용 -->
<pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 상용(PROD) 프로필로 실행될 때만 파일로 저장해라! -->
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="FILE" />
</root>
</springProfile>
</configuration>
4. 마무리 및 주요 발전 방향
지금까지 서버가 무너지고 에러가 터져도 마지막까지 남아 진실을 알려주는 유일한 단서이자 블랙박스, Logback 시스템을 통한 체계적인 로그 레벨링과 파일 로테이션(Rolling) 최적화 기법에 대해 다루어 보았습니다. 이제 여러분의 서버는 과거의 장애를 완벽하게 추적할 수 있는 튼튼한 체계를 갖추었습니다.
로그도 잘 남겼으니, 이제 코드를 본격적으로 짜볼 시간입니다. 그런데 앞선 (MVC 패턴) 코드 예제에서 컨트롤러 위에 @RequiredArgsConstructor를 붙여놓고 주방장(Service) 객체를 그냥 선언만 했는데, 도대체 누가 저기에 진짜 서비스 객체를 생성(new)해서 넣어준 걸까요? 우리는 new UserService()라고 적은 적이 한 번도 없습니다.
다음 설명에서는 객체의 생성과 죽음을 스프링이 대신 통제하는 스프링 프레임워크의 존재 이유, '의존성 주입(DI)과 IoC 컨테이너'에 대해 다뤄보겠습니다.
'Framework > Spring Boot' 카테고리의 다른 글
| [Spring Boot] JPAHibernate 기초 객체와 관계형 DB의 패러다임 불일치를 부수다 (0) | 2026.10.07 |
|---|---|
| [Spring Boot] 의존성 주입DI과 IoC 컨테이너 객체의 생성 소멸 자동화 (0) | 2026.10.06 |
| [Spring Boot] application.yml 완벽 가이드 (+ Profile 환경 분리) (0) | 2026.10.04 |
| [Spring Boot] Spring Initializr 실무 의존성 설계 가이드 (3) | 2026.10.03 |
| [Spring Boot] 스프링부트 3 시작하기 (0) | 2026.10.02 |