지금까지 진행했던 사이드프로젝트(들)에서는 로그 관리를 크게 중요하게 생각하지 않았다. 이유는 아래와 같다.
프론트와 백의 HttpMessage에 대해 과하게 고민했던 과거가 있다. 그 결과 HttpResponseCode와 HttpResponseMessage를 상세하게 나타냈고 로그를 살펴보지 않아도 어떤 상황이 발생했는지 알 수 있었다.
운영/개발 환경을 분리하지 않는 사이드 플젝 특성상 개발자는 모든 권한을 가지고 있다.
안되는게 있으면 데이터베이스를 까보고, sql을 날리면 된다. 로직이 안되면 tail -f를 날려서 로그를 보면 된다.
하지만 회사에서 개발을 진행하다보니, 아래와 같은 이유로 로그의 중요성을 뼈저리게 깨닫게 되었다.
개발자가 아무리 Exception 분기를 열심히 해도 예상치 못한 오류는 날 수 밖에 없다. 그리고 그런 예외 메시지가 사용자에게 노출되는 것은 보안취약점이 된다.
따라서 Exception이 발생하면 (당연한 예외처리 외에는) 사용자입장에서 뭉뚱그려 표현이 된다. 따라서 어떤 문제가 발생했는지 알기 힘들다.
개발/검증 과정에서는 테스트를 할 수 있지만 운영 과정에서 문제가 발생했을때는 직접 쿼리를 날려볼 수 없다.
또한 MCI, EAI, FEP 등으로 내/외부 협력업체와 데이터를 주고받는 경우 문제의 원인을 찾기 더욱 힘들다.
30초/1분/1시간/1일 등 다양한 간격으로 실행되는 배치 프로세스가 상당히 많다. 그리고 배치 프로세스를 관리하는 툴이 있고, 해당 툴은 로그를 기반으로 운영된다.
따라서 성공적으로 배치가 돌았는지, 문제가 발생했는지를 확인하는 방법은 로그를 상세하게 적는 방법 뿐이다.
회사에서 로그파일이 날짜/파일/배치 등 세부적으로 구분되어있는게 너무 편했다.
지금 하고있는 개인 프로젝트는 배치 기반의 크롤링이 메인이라 로그를 한번 제대로, 효율적으로 관리를 해보려고 한다.
스프링부트 프로젝트에서 기본으로 로그를 찍으면 하나의 로그파일에 기록된다.
resources/logback-spring.xml 파일을 만들어 콘솔/파일에 로그가 날짜별로 분리되도록 하자.
<!-- logback-spring.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 로그 저장 경로 -->
<property name="LOG_PATH" value="./log"/>
<!-- 콘솔 로그 출력 -->
<appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS, Asia/Seoul} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 날짜별 파일 로그-->
<appender name="DailyRollingFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 날짜별 파일명: batch.2025-07-17.0.log -->
<fileNamePattern>${LOG_PATH}/batch.%d{yyyy-MM-dd, Asia/Seoul}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>50MB</maxFileSize> <!-- 하루에 50MB 넘으면 .1, .2 등으로 나뉨 -->
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>21</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS, Asia/Seoul} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 로그 출력 대상 지정 -->
<root level="INFO">
<appender-ref ref="Console"/>
<appender-ref ref="DailyRollingFile"/>
</root>
</configuration>
위 사진처럼 설정하고 서버에 프로젝트를 올리면, /log/batch.{날짜}.i.log 형식으로 로그가 생성된다. 아래 사진처럼!

로그를 확인할 때, 터미널을 통해 cli로 확인하는 것 보다는 AWS CloudWatch를 이용하자.
로그 검색도 편하고, 추후 CPU 사용량같은 metric 지표와 연계하기도 편하다.
먼저 IAM Role을 만들어야 한다. IAM Role은 EC2가 가지는 Role이고, 해당 Role 안데 CloudWatch에 접근할 수 있는 정책(Policy)를 할당하는 개념이다.

위 사진처럼 EC2 인스턴스에 IAM Role을 부여하자.




CloudWatch에 접근할 수 있는 Policy이다.


위 사진처럼 새로운 IAM Role이 생성된 것을 알 수 있다.

이제 내 EC2는 CloudWatch로 로그를 보낼 수 있게 되었다.
CloudWatch Agent는 EC2에서 발생하는 metric과 log를 AWS CloudWatch로 전송하는 역할이다.
cd ~
wget https://s3.amazonaws.com/amazoncloudwatch-agent/ubuntu/amd64/latest/amazon-cloudwatch-agent.deb
sudo dpkg -i -E ./amazon-cloudwatch-agent.deb

위 사진처럼 opt/aws/amazon-cloudwaych-agent 디렉터리가 생성되었으면 성공이다.
cloudwatch-config.json 파일을 생성하여, CloudWatch Agent가 어떤 로그 파일을 수집하여 CloudWatch로 보낼 지 정의하여야 한다.
/home/ubuntu/cloudwatch-config.json
해당 경로의 파일을 생성하여 아래처럼 작성한다.
# cloudwatch-config.json
{
"logs": {
"logs_collected": {
"files": {
"collect_list": [
{
"file_path": "/home/ubuntu/log/batch.*.log",
"log_group_name": "/bus/batch",
"log_stream_name": "{instance_id}/batch"
"timestamp_format": "%Y-%m-%d %H:%M:%S, timezone=Asia/Seoul"
}
]
}
}
}
}
file_path를 포함한 각종 설정은 본인의 로그 설정에 맞게하면 된다.
"timestamp_format": "%Y-%m-%d %H:%M:%S, timezone=Asia/Seoul" 이 부분이 중요한데, Cloudwatch의 로그스트림은 타임스탬프와 2시간 이상 차이나면 받아들이지 못한다. 현재 내 서버의 로그는 UTC가아닌 UTC+9이므로 Cloudwatch의 설정도 변경해준다.
sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \
-a fetch-config \
-m ec2 \
-c file:/home/ubuntu/cloudwatch-config.json \
-s

위처럼 로그가 뜨면 실행이 된 것이다.
추가로
sudo tail -f /opt/aws/amazon-cloudwatch-agent/logs/amazon-cloudwatch-agent.log
을 실행하여 로그가 생성되고 있는 것을 확인하면 된다.
위 과정에서 문제없이 진행되었다면 AWS 콘솔 - Cloudwatch - 로그 그룹에서 내 로그를 확인할 수 있다.



이렇게 하면, 일일이 터미널에 접속하지 않아도 AWS CloudWatch에서 로그를 관리할 수 있다.
그러나 로그스트림에 보면 날짜별이 아닌 인스턴스ID별로 로그가 쌓이는 것을 알 수 있다.
cloudwatch-config.json의 "log_stream_name": "{instance_id}/batch"의 값이 날짜별로 바뀌지 않기 때문이다.
해당 문제를 해결하기 위해 챗짱의 힘을 빌려 파일명을 파라미터로 사용하고자 했으나 간단히 적용되지 않았다..

날짜별로 로그를 분리하였는데 하나의 스트림으로 로그가 쌓이는건 내가 원하는 바가 아니었다. 따라서 나는 Cloudwatch에서 로그를 날짜별로 분리하여 스트림으로 쌓아보기 위해 아래와 같은 방법을 찾았다.
log_stream_name의 값에 날짜별로 구분할 수 있는 파라미터를 넣어주면 된다.
file_path해 해당하는 로그파일을 수집하여 log_stream_name 로그스트림에 넣어주겠다는 뜻이다.
# ~/cloudwatch-config.json
{
"logs": {
"logs_collected": {
"files": {
"collect_list": [
{
"file_path": "/home/ubuntu/log/batch.$DATE*.log",
"log_stream_name": "{instance_id}/batch-$DATE",
"log_group_name": "/bus/batch",
"timestamp_format": "%Y-%m-%d %H:%M:%S, timezone=Asia/Seoul"
}
]
}
}
}
}
cloudwatch-config.json 템플릿에 날짜를 치환하여 생성해주는 스크립트를 생성한다.
해당 스크립트가 실행되면 cloudwatch-template.json 파일이 해당 날짜에 맞게 변경이 된다.
cloudwatch-template.json 파일과 동일한 경로인
/home/ubuntu/update-cloudwatch-config.shell에 파일을 생성한다.
#!/bin/bash
# 스크립트 실행 로그 기록
LOG_FILE="/home/ubuntu/cloudwatch-update.log"
# KST 기준 날짜 계산
DATE=$(TZ=Asia/Seoul date +%F)
# 로그 기록 시작
echo "[$(date)] Updating config with DATE=$DATE" >> $LOG_FILE
# 템플릿에서 $DATE를 실제 날짜로 치환하여 config 생성
sed "s/\\$DATE/$DATE/g" /home/ubuntu/cloudwatch-template.json > /home/ubuntu/cloudwatch-config.json
# 결과 확인용 로그 출력
echo "----- Generated Config Start -----" >> $LOG_FILE
cat /home/ubuntu/cloudwatch-config.json >> $LOG_FILE
echo "----- Generated Config End -----" >> $LOG_FILE
# CloudWatch Agent 설정 반영 및 재시작
sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \
-a fetch-config \
-m ec2 \
-c file:/home/ubuntu/cloudwatch-config.json \
-s >> $LOG_FILE 2>&1
그리고 아래 명령어로 쉘 실행권한을 준다.
chmod +x /home/ubuntu/update-cloudwatch-config.sh

아래처럼 실행권한이 생성되었음을 확인하자.
crontab -e
0 15 * * * /home/ubuntu/update-cloudwatch-config.sh
위의 스크립트를 입력하여 매일 15시에 실행되도록 한다. UTC 기준 15시이므로 KST 0시에 실행이 될것이다.

crontab -l 명령어를 이용하여 위처럼 cronjob이 잘 등록되었으면 완료이다.

이제 날짜별 로그를 AWS CloudWatch에서 관리할 수 있게 되었다!