로그 개선을 하려다 개발자로서의 목표가 생겼어요
현재 조직은 Product Engineer 체제로 각자가 실제 PRD ~ 배포까지의 사이클을 가진 조직이에요. 각자가 발산하는 영역은 넓어졌지만, 누군가 Product Engineer를 위해 시스템 안전성, 보안 거버넌스, 배포 파이프라인 등의 전반적인 관리를 해주지는 않아요. 각 개발자들이 책임져야하는 영역입니다.
이 책임이라는건, 관심이 없으면 지속적으로 개선해나가기 힘들어요. 특히 Observability 또한 지속적으로 Alert 구축 및 개선, SLO/SLA 정의나 모니터링 도구들도 계속해서 최신화 해야하는 등의 역할이 많을 것 같은데요. 관측성을 전담하는 사람이 없는 상황이에요. 참고로 저는 백엔드 개발자고, 입사 6개월 차에요.
어느날 무언가 이상하다고 느꼈어요. 우리가 가진 threshold 기반 알림은 RDS CPU 50% / 80% 임계치 알림이 전부였고, 가진건 CloudWatch밖에 없었습니다. 별도 모니터링 도구도 없고 트레이싱 수단도 없었고 로그는 평문이었어요.
이 글은 입사 후 무언가 이상함을 느낀 주니어의 삽질 기록이에요. 도구가 없는 상태에서 무엇부터 손댈지 정하고, 로그 한 줄을 고쳐서 지표로 만들고, 그러다 데이터베이스가 인터넷에 열려 있는 걸 발견해서 거기까지 간 이야기예요. 장황하게 Obs, SRE를 얘기했지만, 크게 별로 한 건 없을 수도 있습니다 ㅋㅋ. 끝난 작업도 아니에요. 지금도 새로 들어오는 코드는 파이프라인을 비켜 가고, 무엇을 더 해야 하는지도 다 알지 못해요. 다만 문제를 발의하고 기능 조직 전체에 경각심을 심어 조금 더 관심을 가지도록 했다고는 볼 수 있겠네요.
여튼 왜 이런 작업들을 했는지 기록을 남기려고 해요.
제품이 빠르게 성장할 것이라고 확신했어요
저는 인플루언서와 광고주를 잇는 인플루언서 광고 플랫폼을 만드는 회사에서 제품을 만들고 있어요.
인플루언서 광고 시장에는 최근 1년 사이 새 플랫폼이 계속 들어왔고, 이미 자리를 잡은 커머스 플랫폼도 이 안으로 들어왔어요.
우리 조직도 1월 대비 매출은 20배가 넘는 성장을 하였지만, 다른 제품들도 마찬가지라고 생각해요. 정말 많은 제품들이 자리잡았고, 또 생겨나고 있는 상황이에요.
PMF 자체는 찾았고, 다른 조직들도 이 PMF를 해자라고 생각하고 뛰어들고 있어요. 저는 여기서 성장할 것이라는 확신이 들었어요.

브랜드의 캠페인 목표에 맞는 인스타그램 디지털 매거진과 크리에이터를 AI로 연결하는 광고 서비스예요.
매거진 네트워크 70곳 이상
매쉬업벤처스, 카카오벤처스 등 참여

숏폼 전문 인플루언서 마케팅 플랫폼이에요. 뷰티로 시작해 건기식 같은 다른 카테고리로 넓히고 있어요.
출시 7개월 만에 월매출 15배
연결 인플루언서 5,613명

브랜드 대신 릴스를 만들어 주는 제작 플랫폼이에요. 뷰티로 시작해 식품과 영양제, 이너뷰티까지 넓히고 있어요.
감성형, 정보형, 방문형 세 갈래
원고료 20만원부터, 2차 활용 평생 무료

블로그와 인스타그램, 유튜브를 아우르는 국내 최대 규모의 인플루언서 마케팅 플랫폼이에요.
인플루언서 회원 150만명
누적 캠페인 126만건, 월 1.8만건

인플루언서를 찾고 관리하고 성과를 증명하는 구간을 데이터로 잇는 B2B 서비스예요.
누적 인플루언서 DB 1,700만건
SNS 콘텐츠 3억건 분석

광고주가 직접 체험단을 모집하고 운영하는 플랫폼이에요. 팔로워를 부풀린 계정을 걸러내는 쪽에 힘을 줬어요.
활동 인플루언서 5만명 이상
도달과 노출을 API로 확인

브랜드 성장에 필요한 콘텐츠를 크리에이터가 만들어 내보내는 구조를 자동화한 플랫폼이에요.
크리에이터 모집과 집행을 한 흐름으로
브랜드와 크리에이터 양쪽 진입점
우리는 인플루언서 광고를 캠페인이라고 불러요. 위에서 언급했듯이 시장이 이렇게 움직이는 동안 우리 캠페인 수도 같이 늘었어요.
1월 대비 매출은 20배 가량, 캠페인 수는 1월 대비 5배가 넘게 성장했어요.
여기서 중요한 건 곡선 자체가 아니라 캠페인이 늘 때 뒤따라 붙는 것들이에요. 늘어나는 게 트래픽만이 아니거든요.
운영 이슈 대응과 개발 속도 요구가 같이 붙어서 셋이 비례해 늘어요.
캠페인마다 브랜드가 다르고 사용 방식이 다르니 문제가 나타나는 모습도 그만큼 여러 갈래로 갈리고요.
성장을 가속하려면 시스템이 버티는지 미리 확인하고 점검할 수 있는 수단이 있어야 한다고 생각했어요.
우리의 일하는 방식에 맞게, 각자가 발산을 하더라도 사전에, 사후에 쉽게 점검할 수 있도록 말이에요.
알림은 전부 사후 대응이에요
입사하고 6개월을 되돌아보니 임계치 알림은 RDS 기반이 거의 전부였고, 나머지는 특정 에러가 났을 때 슬랙으로 오는 알림이었어요.
둘을 나란히 놓고 보니 공통점이 하나 있었고요. 이미 상황이 벌어진 후에야 알 수 있다는 거죠.


여기서 걸리는 게 있었어요. 우리 시스템은 대부분 종단이 RDS예요. 그러니 무슨 일이 나든 RDS 지표가 먼저 흔들려 보이고, 전부 RDS 피크 이슈처럼 보이게 되어 있어요. 정작 사용자 경험이 어디서 무너지는지는 그 지표로 알 수 없어요.
실제로 그런 일이 있었어요.
알림이 발생하지 않아도 사용자의 UX는 저하될 수 있어요. 사용자가 문제를 겪고 있을 수도 있고요.
임계치 아래에서 조용히 실패하는 것들, 알림이 울리지 않는 지금 이 순간에도 누군가는 문제를 겪고 있을 수도 있어요. 우리는 그걸 알 수 있을까요?
알림이 울려도 쉽게 파악할 수 있는 구조가 아니에요
별도의 모니터링 도구 없이 CloudWatch에 의존하다 보니, 장애가 났을 때 관측하는 방법이 개발자마다 달랐어요. 누구는 로그 그룹부터 열고 누구는 메트릭부터 보고 누구는 DB 화면부터 열었고요. 도구가 없으니 각자 자기 방법을 만든 거예요.
문제는 각자 익숙한 축만 본다는 거고요. 그러면 아무도 안 보는 축이 생기고, 원인을 두고 의견이 갈렸을 때 어느 쪽이 맞는지 가릴 공통 근거도 없어요.

서버 장애는 보통 로그와 메트릭을 조합해서 원인을 특정해요. 그런데 우리 로그는 평문이었고 트레이싱 수단도 없었어요. 조합할 재료의 한쪽이 통째로 없었던 셈이에요.
이벤트 93만 줄, 스캔량 315MB. 그 안에 요청 하나를 이어줄 식별자는 0건이었어요.
5xx 집계도 0이었는데, 서버 오류가 없었다는 뜻이 아니라 상태 코드가 구조화된 필드로 안 찍혀서 셀 대상 자체가 없었다는 뜻이에요.
여기에 하나 더 붙어요. 요즘은 디버깅도 AI를 붙여서 하는데, 사람이 한 번에 파악 못 하는 로그를 AI가 한 번에 파악할 리가 없어요. 연관을 못 잡으니 명령 가짓수만 늘어나고 그만큼 오래 걸려요.
문제는 생각보다 컸어요
클로드와 함께 사전 조사를 시작했어요. 로그 포맷이 Plain Text인 문제와 포맷 불일치부터 시작해서, 로그 위생과 시스템 배포 안전성 문제, 인프라 전반의 보안 문제도 있었어요. 긴 호흡의 작업이 되겠다는 직감이 들었어요. 그래서 조사한 내용을 그때그때 컨텍스트로 남기고, 우선순위를 세워 태스크 단위로 쪼개서 가기로 했어요.
우선순위를 정했어요
우선순위는 이렇게 정했어요.
- 0순위 무조건 해결해야 하는 보안 이슈
- 1순위 사이드 이펙트가 작고 빠르게 적용할 수 있는 것
- 2순위 최소한의 작업으로 기준을 정할 수 있는 것
0순위는 팀 내에서 함께 해결해야 하는 문제였어요. 지금 안 쓰는 시스템이 무엇인지부터 가려내야 하는데 입사 6개월 차가 혼자 판단할 수 있는 게 아니었고, 네트워크를 바꾸려면 다운타임도 필요했거든요. 이 부분은 팀 내부 합의가 필요했어요.
1순위와 2순위를 모두 만족하는 건 로그 개선이었어요. 시스템 로그는 추상화된 미들웨어에서 관리하게끔 하면 되니까, 포맷을 거기서 강제하고 기존 코드를 물리기만 하면 돼요. 로그 개선을 시발점으로, 관측 가능성을 개편하고 우리에게 필요한 로그 기반 알림들을 구축할 수 있겠다 생각했어요. 기존에는 로그 기반 알림이 거의 없었거든요.
기존 코드들에 흩어진 출력들을 찾고 수정하는 작업은 클로드 같은 AI가 가장 잘하는 일이에요. 구조화된 기준만 잡아주면 언어·프레임워크·라이브러리마다 다르게 쓰인 모든 출력을 찾고 개선하기 쉬워요. 그래서 사이드 이펙트가 거의 없고 빠르게 적용할 수 있다고 봤어요.
여기까지만 되어도 trace가 생겨요. 기준이 하나로 맞으니 사람이 로그를 한 번만 확인해도 요청 내 흐름들을 한 번에 볼 수 있어요.
AI도 마찬가지예요. 연관관계가 있는 그룹 몇 개만 골라 trace 기반 검색으로 디버깅이 훨씬 빨라질 수 있어요.
이 글에서 관측성이라고 부르는 건 로그를 많이 쌓는 일이 아니에요. 질문에서 증거를 거쳐 행동에 닿는 시간을 줄이는 것이에요.
모니터링이 미리 정해둔 이상을 놓치지 않는 쪽이라면, 관측성은 처음 보는 문제의 범위를 남아 있는 증거로 좁히는 쪽이에요.
로그 포맷부터 통일했어요
기존 로그는 Plain Text라 검색이 불편했어요. 필드 단위 조회가 안 되니 문자열이나 정규식으로 매칭하거나, 매번 parse로 즉석에서 뜯어야 했거든요.
그리고 각자 남기는 로그가 달랐어요. 애플리케이션은 Django 기본 포맷, 엣지는 nginx combined, 라이브러리는 또 제각각이었고요. 그래서 누가 검색해도 같은 메타데이터로 찾을 수 있게 포맷부터 다시 정의했어요.
Internal Server Error: /v1/campaigns/12/apply
192.0.2.10 - - [22/Jun/2026:08:52:11 +0900]
"GET /v1/users/me?email=a%40b.com HTTP/1.1"
200 1834 "-" "Mozilla/5.0"
{
"timestamp": "2026-06-25T02:11:07.418+00:00",
"request_id": "452f5593dd0cdcfc95d4...",
"trace_id": "9cab7561c59dc034f854...",
"service": "link-server",
"release": "a1b2c3d",
"method": "GET",
"path": "/v1/users/me?email=***",
"status_code": 500,
"status_class": "5xx",
"duration_ms": 1842
}
포맷을 정의하면서 trace도 같이 만들었어요. 얻은 게 세 가지예요.
- ① 누가 조회해도 같은 필드로 같은 결과가 나와요.
- ② 흩어진 로그를 한 번에 이어서 볼 수 있어요.
- ③ 어느 배포에서 난 문제인지
release로 추적할 수 있어요.
개선 이후 프로덕션 로그 그룹 다섯 개에서 0건이던 request_id가 배포 이틀 뒤 엣지 로그 기준 99.99%가 됐어요. 셀 수 없던 5xx도 숫자로 잡히기 시작했고요.
한 줄이 지표가 됐어요
포맷이 맞으니 질문하는 방식이 달라졌어요. 로그는 365일 보관이라 개선 전 로그가 그대로 남아 있어요. 그래서 같은 질문을 양쪽에 던져봤어요.
5xx가 어디서 났고, 그 요청에 무슨 일이 있었나. 질문은 이거 하나예요.
# 로그가 어떻게 생겼는지부터 읽어야 해요
parse @message /(?<m>[A-Z]+) (?<p>\S+) =>
generated \d+ bytes in (?<ms>\d+) msecs
\(HTTP\/1\.1 (?<code>\d{3})\)/
| filter code like /^5/
| stats count() as n by bin(1d), p
6/16 /blocks 3,875
6/16 /users 525
# 그 요청에 무슨 일이 있었나
→ 이을 방법이 없어요
filter status_class="5xx"
| stats count() as n by bin(1d), path
8/23 /smartstore/products 12
8/29 /blocks 11
# 한 건 집어서 그대로 따라가요
filter request_id="2bc354c812081ccb..."
03:24:59.567 ERROR Internal Server Error
03:24:59.568 INFO PUT /smart.. - 500
왼쪽에서 막힌 이유는 이거예요. 파이썬 traceback이 CloudWatch에 한 줄씩 서로 다른 이벤트로 들어가 있었어요. 순서도 안 맞고, 어느 요청 것인지 알려주는 것도 없어요.
raise self.ResponseError(self.http_response.status_code, ...)
Traceback (most recent call last):
utils.payments.client.PaymentClient.ResponseError: 503
AI도 마찬가지예요. 굳이 request_id와 trace_id의 역할을 알려주지 않아도 자연스레 트레이스 수단으로 인식하고 디버깅을 더 빠르게 진행할 수 있어요. 그리고 이 한 줄이 그대로 지표가 돼요. status_class는 요청 하나에 정확히 한 번 찍히는 마커라, 메트릭 필터로 세기만 하면 요청 수가 되고 duration_ms를 얹으면 p99가 나와요.
여기가 시발점이에요. 우리에게 필요한 알림은 이 지표 위에 하나씩 붙여나가면 돼요. 무엇을 신호로 볼지가 앱이 아니라 로그 한 줄 위에 있으니까요.
로그 개선 과정에서 같이 진행한 것들이에요.
- ① 중복 로그 제거. 같은 요청이 앱, nginx, uWSGI 세 군데에 남고 지표용 EMF까지 한 줄 더 붙어서, 로그량이 한때 개선 전의 3.7배였어요. 중복 로그를 걷어내고 1.6배 정도로 최적화했어요.
- ② 로그 로테이션 설정. 프로덕션이 2년, Lambda는 무기한이었어요. 지금은 프로덕션 1년, 개발 30일이고 더 줄일지는 논의가 필요해요.
- ③ 로그 위생. 로그가 남는 전 구간에 PII 같은 민감정보를 마스킹해요.
로그를 켜니까 DB도 보였어요
데이터베이스 로그도 활성화하고 나니 이상한 로그가 보였어요. RDS superuser로 누군가 접속을 시도한 흔적이 있었어요. 전수 조사를 해보니 해외 주소에서 들어온 시도들이 종종 보였고요. 더 큰 문제는 성공한 접속은 로그에 남지 않는다는 점이었어요. 뚫렸는지 아닌지를 판단할 근거가 아예 없었어요.
앞에서 각자 익숙한 축만 보다 보면 아무도 안 보는 축이 생긴다고 했는데, 누군가 DB에 붙어보고 있다는 사실 자체를 우리가 전혀 모르고 있었어요.
제일 먼저 하고 싶었던 건 당연히 차단이었어요. 그런데 그러면 안 됐어요. 누가 이 데이터베이스에 붙는지 아무도 몰랐거든요. 그래서 며칠 더 지켜본 다음에 막았어요. 차단은 언제든 되돌릴 수 있지만, 무엇이 깨졌는지는 사후에 복원할 수 없으니까요.
실험 1. 다운타임 없이 인터넷에서 뗄 수 있을까
정리하면 세 겹이 전부 퍼블릭이었어요. DB가 퍼블릭 서브넷에 있고, 공인 IP를 달고 있고, 앞을 지키는 보안그룹이 5432를 0.0.0.0/0으로 열어두고 있었어요. 서브넷 단위로 거르는 NACL은 기본값 그대로라 아무것도 안 막고 있었고요.
셋은 AND라서 하나만 아니어도 바깥에서는 안 닿아요. 그러니 어디를 먼저 끊을지가 선택이었어요.
그래서 위험을 얼마나 줄이느냐가 아니라 틀렸을 때 얼마나 빨리 되돌릴 수 있느냐로 순서를 정했어요. 되돌리기가 싼 것부터 손대면 아니다 싶을 때 원상복구가 빠르니까요.
개발 환경에서 이 순서로 돌렸어요.
결과는 좀 허탈했어요. 공인 경로로 들어오는 접속이 거의 없었거든요. 컨테이너도 EC2도 원래부터 사설 주소로 붙고 있었어요. 공인 IP는 우리는 아무도 안 쓰면서 공격자한테만 열려 있던 문이었어요. 플래그 해제는 1분 걸렸고 재부팅도 장애 조치도 없었어요. 앱 접속이 끊긴 건 0건이고요.
다만 플래그 해제는 최종 형태가 아니에요. 한 겹에 의존하는 상태라 누가 되돌리면 다시 열려요. 그래서 이건 지금의 노출을 줄이는 임시 조치로 두고, 서브넷 이전을 최종으로 잡았어요.
실험 2. 참조처를 다 고쳐야 할까
Aurora는 이미 만들어진 클러스터의 서브넷 그룹을 바꿀 수 없어요. modify-db-cluster에 그 옵션이 아예 없어요. 그러니 프라이빗 서브넷에 새로 만들어 옮겨가야 하는데, 그러면 접속 주소가 바뀌어요. 접속 문자열은 람다 환경변수와 인프라 코드와 태스크 정의에 흩어져 박혀 있고요.
Aurora는 클론을 지원해요. 스토리지를 복사하지 않고 쓰기가 일어난 블록만 뒤늦게 나눠 갖는 방식이라, 크기와 무관하게 몇 분이면 만들어져요. 그래서 프라이빗 서브넷에 클론을 만들고 원본과 클러스터 이름을 맞바꿨어요. 엔드포인트 주소가 클러스터 이름을 따라가니까요.
다운타임이 필요한 이유는 쓰기 때문이에요. 클론은 만든 시점까지의 데이터만 갖고 있어요. 그래서 이름을 바꾸는 동안 앱이 옛 클러스터에 계속 쓰면, 그 쓰기는 새 쪽에 없어요. 그래서 그 구간만 앱을 내려서 새 쓰기를 막았어요. 읽기만 하면 이럴 필요가 없지만, 쓰기가 섞이면 정합성이 깨져요.
결과적으로 컨테이너와 EC2와 람다 설정은 한 곳도 안 바꿨어요. 같은 VPC 안이라 사설 경로로 붙고 주소도 그대로거든요. 바뀐 건 개발자 로컬 접속 경로뿐이에요. 클론이 승계하지 않는 것도 있었어요. CloudWatch 로그 내보내기와 삭제 방지가 안 따라와요. 컷오버 직후에 관측이 끊긴다는 뜻이라, 가장 필요한 시점에 못 보게 돼요. 만들 때 같이 넣어야 해요.
여기까지가 개발 환경에서 돌린 한 사이클이에요. 다만 VPC 안에 넣은 람다는 아웃바운드 경로가 없어요. DB로 가는 사설 길은 얻지만 인터넷으로 나갈 길이 사라지거든요. 외부 API를 호출해야 하니 NAT Gateway를 뒀어요. 서울 리전에서 띄워두기만 해도 월 43달러쯤이고, 처리한 트래픽에 또 붙어요. 개발 환경은 가용영역마다 두지 않고 하나만 뒀어요.
최종 형태는 이래요. DB를 프라이빗 계층에 두고, 주소가 아니라 리소스 소속으로 접근을 정해요. 보안그룹에 CIDR 규칙을 0개로 두고 SG 참조만 남기면, 누가 붙을 수 있는지가 IP 목록이 아니라 어느 서비스인지로 바뀌어요.
한 번 세우고 끝나는 게 아니었어요
포맷을 기준으로 만든 순간 지켜야 할 계약이 하나 생겼어요. 메트릭 필터가 status_class와 request_id를 읽으니까, 누가 필드 이름 하나만 바꿔도 필터가 조용히 안 맞고 지표가 0으로 떨어져요. 에러도 안 나고요.
그리고 새로 들어오는 코드는 계속 파이프를 비켜 가요. 로그 개선이 끝났다고 생각했는데, 결제 이슈를 파다가 앱 전역에 파이프라인 밖으로 나가는 출력이 174개 남아 있는 걸 발견했어요. 완료 판정이 미들웨어 안쪽에만 해당됐던 거예요.
경계가 늘면 봐야 할 자리도 늘어요. 마스킹을 앱에 걸었는데 nginx는 앱 앞에서 로그를 남기니까 엣지에는 그대로 남아 있었어요. 볼륨과 보관 기간도 마찬가지예요. 한 번 걷어냈지만 캠페인이 늘면 다시 차올라요.
알람은 특히 그래요. 안 우는 알람은 건강한 건지 죽은 건지 알람만 봐서는 몰라요. 그래서 요청 수가 5분간 0이면 우는 알람을 따로 뒀어요. 서비스를 보는 게 아니라 관측 파이프 자체를 보는 알람이에요.
돌아보면 늦었어요
자료 조사와 현행 조사는 많이 했어요. 그런데 그게 무엇을 해야 하는지까지 알려주지는 않았어요. 무엇을 버려야 하는지는 더 몰랐고요. 조사가 쌓일수록 할 일 목록만 길어졌어요.
기존 업무와 병행한 것도 컸어요. 이 작업은 제품 로드맵에 없으니까, 다른 일이 밀리면 자연스럽게 뒤로 갔어요.
그리고 로그를 고친 다음에 실측 사이클을 2~3주 돌렸어요. 정말 반영됐는지, 회귀는 없는지 계속 다시 쟀거든요. 지금 돌아보면 그만큼 필요했는지 모르겠어요. 처음 만지는 영역이라 확신이 없어서 계속 확인한 건데, 그게 안전장치였는지 그냥 늦춘 건지는 아직도 딱 잘라 말하기 어려워요.
관측성은 한 번 세우는 구조물이 아니라 제품과 같이 커지는 것이에요.
로그도 알람도 인프라 위생도 기능이 아니라서 로드맵에 안 올라오고, 안 올라오면 아무도 안 해요. 그런데 안 하면 계속 쌓이고, 쌓인 게 터지면 그때는 제품이 멈춰요. 목적 조직의 우선순위와 별개로 기능 조직이 상시로 들고 가야 하는 일이라고 봐요.
캠페인 수는 2026-08-25 프로덕션 읽기 전용 복제본에서 조회했어요. 등록 기준이고 내부 테스트 캠페인은 뺐어요.
8월은 25일까지의 진행 중 실측이고, 9월 이후는 거기에 월 18% 성장을 적용한 예상치라 실측이 아니에요. 로그 실측은 2026-06-22 08:49 KST부터 한 시간 창이에요.































