[모두의 플리] Logback 로그를 Spring Batch로 S3에 백업하기

배경

애플리케이션 로그는 개발 중에는 콘솔에서 바로 확인하면 충분하다.

하지만 운영 환경에서는 프로세스가 재시작되거나 인스턴스가 교체되면 로컬 로그를 잃을 수 있고, 하나의 로그 파일이 계속 커지면 디스크 공간도 부족해질 수 있다.

이번 작업에서는 로그를 다음 세 단계로 나누어 관리했다.

애플리케이션 로그 출력
  -> Logback이 로컬 파일을 날짜·크기 기준으로 압축
  -> Spring Batch가 완성된 압축 파일을 S3에 백업

중요한 점은 Logback과 Spring Batch가 같은 일을 하는 것이 아니라는 것이다.

  • Logback: 현재 로그를 기록하고 로컬 보관 정책에 따라 회전한다.
  • Spring Batch: 회전이 끝난 로그 파일을 찾아 원격 저장소에 백업한다.
  • S3: 인스턴스가 사라진 뒤에도 로그를 보존한다.

테스트와 운영 환경의 로그 출력 분리

logback-spring.xml에서는 Spring Profile에 따라 Appender를 다르게 구성했다.

test 프로필
  -> CONSOLE

test 이외의 프로필
  -> CONSOLE + FILE

테스트를 실행할 때마다 파일 로그와 압축 로그가 생성되면 테스트가 느려지고 작업 디렉터리에 불필요한 파일이 쌓일 수 있다.

그래서 테스트에서는 콘솔만 사용하고, 실제 실행 환경에서는 콘솔과 파일에 함께 기록하도록 했다.

로그 파일 회전 정책

Spring Boot가 제공하는 Logback Rolling Policy 설정을 활용했다.

logging:
  file:
    name: logs/mopl.log
  logback:
    rollingpolicy:
      file-name-pattern: logs/archive/mopl.%d{yyyy-MM-dd}.%i.log.gz
      max-file-size: 50MB
      max-history: 14
      total-size-cap: 2GB
      clean-history-on-start: false

로그는 날짜가 바뀌거나 한 파일이 50MB를 넘으면 새로운 파일로 분리된다.

분리된 파일은 gzip으로 압축하고 로컬에는 최대 14일, 전체 2GB까지만 보관한다.

날짜 기준만 사용하면 트래픽이 많은 날에는 한 파일이 지나치게 커질 수 있다. 반대로 크기 기준만 사용하면 특정 날짜의 로그를 찾기 어렵다.

그래서 날짜와 크기 기준을 함께 사용했다.

활성 로그가 아니라 완성된 압축 파일만 백업하기

현재 기록 중인 mopl.log를 업로드하면 업로드 도중에도 파일 내용이 바뀔 수 있다.

이번 Batch는 Logback의 회전이 끝난 .log.gz 일반 파일만 찾는다.

백업 대상: logs/archive/*.log.gz
제외 대상: 현재 기록 중인 mopl.log
제외 대상: 디렉터리, 심볼릭 링크, 그 밖의 확장자

파일 시스템과 S3 작업은 데이터베이스 트랜잭션처럼 한 번에 롤백할 수 없다.

따라서 이 Step에는 DB 트랜잭션 대신 ResourcelessTransactionManager를 사용하고, 각 파일의 성공과 실패를 코드에서 명시적으로 관리했다.

S3 Object Key 구성

백업 파일은 다음 구조로 저장한다.

logs/mopl/{instanceId}/{yyyy}/{MM}/{dd}/{fileName}

예를 들어 여러 서버가 같은 시간에 로그를 생성하더라도 instanceId가 다르면 서로 덮어쓰지 않는다.

날짜 경로를 함께 사용하면 특정 날짜와 서버의 로그를 찾기도 쉽다.

경로를 만들 때는 다음 안전성 검증도 적용했다.

  • 앞뒤 슬래시 정규화
  • 빈 경로와 .·.. 구간 거부
  • 인스턴스 ID의 안전하지 않은 문자를 _로 치환
  • 심볼릭 링크 업로드 거부
  • 일반 파일만 업로드 허용

같은 파일을 다시 올리지 않는 멱등성

Batch는 S3에 같은 Object Key가 이미 존재하는지 먼저 확인한다.

S3에 파일 없음 -> 업로드
S3에 파일 있음 -> skipped 처리

이처럼 같은 작업을 여러 번 실행해도 최종 결과가 달라지지 않는 성질을 멱등성이라고 한다.

배치가 중간에 실패해 재실행되더라도 이미 업로드한 파일을 다시 전송하지 않기 때문에 네트워크 사용량과 중복 저장을 줄일 수 있다.

부분 실패를 숨기지 않기

파일 하나의 업로드가 실패했다고 즉시 반복을 중단하면 뒤에 있는 정상 파일도 백업하지 못한다.

그래서 모든 파일을 끝까지 처리하면서 결과를 집계한다.

discoveredCount: 발견한 파일 수
uploadedCount: 새로 업로드한 파일 수
skippedCount: 이미 존재해 건너뛴 파일 수
failedCount: 업로드에 실패한 파일 수

결과는 Spring Batch의 ExecutionContext에 저장한다.

실패한 파일이 하나라도 있으면 모든 파일을 확인한 뒤 Job을 실패 처리한다. 일부 파일이 실패했는데도 Job이 성공한 것처럼 기록되는 상황을 막기 위해서다.

중복 실행 방지

스케줄 지연이나 수동 실행이 겹치면 같은 백업 Job이 동시에 실행될 수 있다.

Tasklet은 JobExplorer로 더 먼저 시작된 실행이 아직 진행 중인지 확인한다. 이전 실행이 남아 있다면 새 실행을 거부한다.

이 검사는 S3의 파일 존재 확인과 함께 이중 안전장치가 된다.

동시 실행 방지 -> 같은 파일을 동시에 처리할 가능성 축소
S3 존재 확인 -> 재실행되더라도 중복 업로드 방지

스케줄과 기능 활성화

로그 백업은 기본적으로 비활성화하고 환경변수로 켤 수 있게 했다.

LOG_BACKUP_ENABLED=false
LOG_BACKUP_CRON=0 0 4 * * *

기본 Cron은 매일 새벽 4시이며 BatchSchedulerAsia/Seoul 시간대를 적용한다.

잘못된 Cron은 애플리케이션 시작 시 검증해 조용히 잘못된 스케줄로 실행되는 일을 막는다.

로그와 메트릭의 역할

공통 BatchScheduler는 모든 Job에 대해 다음 정보를 기록한다.

시작·완료·실패 로그
Job 실행 시간
SUCCESS·FAIL 누적 메트릭

로그 백업 Tasklet은 여기에 파일 단위 결과와 전체 집계를 추가한다.

로그는 실패한 파일과 예외 원인을 찾는 데 사용하고, 메트릭은 장기적인 성공률과 실행시간 추세를 확인하는 데 사용한다.

테스트한 내용

  • 테스트 프로필에서는 파일 Appender가 비활성화되는지 확인
  • 운영 프로필에서 Console과 File Appender가 함께 등록되는지 확인
  • Rolling Policy의 파일명·크기·보관 기간·전체 용량 확인
  • .log.gz 파일만 검색하는지 확인
  • 같은 S3 Key가 존재할 때 업로드를 건너뛰는지 확인
  • 업로드 일부 실패 시 나머지 파일을 계속 처리하고 Job은 실패하는지 확인
  • 결과 건수가 ExecutionContext에 저장되는지 확인
  • 경로 정규화와 심볼릭 링크 거부 확인
  • 중복 실행 감지 확인
  • S3 업로드 Content-Type과 Content-Length 확인

이번 작업에서 배운 점

로그 파일 회전과 원격 백업은 서로 다른 책임이다.

또한 파일 시스템과 S3 작업은 DB 트랜잭션으로 되돌릴 수 없기 때문에, 멱등성·부분 실패 집계·재실행 정책을 애플리케이션에서 직접 설계해야 한다.

단순히 파일을 업로드하는 기능보다 실패한 뒤 다시 실행해도 안전한 구조를 만드는 것이 Batch 작업에서 더 중요했다.