8년 된 레거시 시스템의 에러를 추적한 과정 - 로깅 한 줄의 힘
0. 들어가며
안녕하세요 :) 시스템컨설턴트그룹 27기 백엔드 개발을 하고 있는 노주희입니다.
운영 중인 서비스에서 에러가 발생했다는 연락을 받았을 때, 가장 먼저 확인하는 것은 로그입니다. 그런데 로그에 아무런 단서가 없다면 어떻게 해야 할까요?
이 글은 성균관대학교 소프트웨어융합대학의 졸업평가 시스템(연구논문작품시스템, 이하 cssys)에서 발생한 파일 업로드 500 에러를 추적하고 해결한 과정을 정리한 것입니다. 로깅이 부재한 레거시 시스템에서 어떻게 원인을 좁혀나갔는지, 그리고 그 과정에서 느낀 점을 공유합니다.
1. 배경
서비스 소개
cssys는 소프트웨어학과 졸업평가 연구논문작품의 서류 제출 및 심사 과정이 이루어지는 시스템입니다. 학생들은 서약서, 제안서, 중간보고서, 최종보고서 등을 이 시스템을 통해 제출하고, 행정실에서는 관리자 페이지를 통해 서류를 관리합니다.
이 서비스는 SCG에서 운영하고 있으며, 첫 커밋이 2018년 1월로 기록되어 있는, 8년 이상의 역사를 가진 레거시 시스템입니다. 정보통신대학의 졸업평가 시스템(iccsys)이 2016년에 만들어졌고 해당 프로젝트를 fork 해서 만든 서비스로 파악됩니다.

기존 기술 스택은 다음과 같습니다.
- 런타임/프레임워크: Node.js + Express 4
- ORM: Sequelize
- DB: MySQL
- 템플릿 엔진: Swig
- 배포: 물리 서버의 VM 위에서 pm2로 수동 배포
- 파일 저장: 서버 로컬 파일시스템
마이그레이션
기존 물리 서버의 디스크 용량 부족 문제로 인해, 2026년 3월 초에 28기 양현준 님과 27기 송영욱 님이 함께 다음과 같은 마이그레이션을 진행했습니다.
| 항목 | Before | After |
| 배포 환경 | VM 위 수동 실행 | Kubernetes + ArgoCD + GitHub Actions CI/CD |
| 파일 저장 | 서버 로컬 폴더 | MinIO Object Storage |
| 라이브러리 | mysql / Sequelize v2 | mysql2 / Sequelize v6 |
| 템플릿 엔진 | Swig | EJS |
마이그레이션 후 행정실에 테스트를 요청했고, 특별한 이상이 없다는 회신을 받았습니다. 3월 6일~7일에 서버 이전 작업이 완료되었고, 서비스가 정상적으로 운영되는 것으로 판단했습니다.
2. 문제 발생
에러 보고 (3월 11일)
서버 이전이 완료된 지 며칠 후, 행정실로부터 메일이 도착했습니다. 관리자 계정으로 학생의 신청서를 업로드하려 했으나, "에러가 발생하였습니다. 시스템 관리자에게 문의해주세요."라는 팝업이 표시된다는 내용이었습니다.

초기 상황 파악의 어려움
이 에러를 받았을 때, 몇 가지 난관이 있었습니다.
첫째, 의미 있는 로그가 없었습니다. ArgoCD를 통해 로그를 확인할 수는 있었지만, 에러 핸들러가 URL만 기록하고 에러 내용 자체는 출력하지 않는 구조였습니다.
// 기존 에러 핸들러 (app.js)
console.error(`[${now}] [ERROR] ${req.method} ${req.originalUrl}`);
// err 객체는 출력하지 않음
둘째, 관리자 페이지에 직접 접근할 수 없었습니다. 관리자 계정으로 접속하면 IP가 로그에 남기 때문에, 보안상의 이유로 개발자의 직접 접근을 지양하고 있었습니다.
셋째, 개발 서버가 존재하지 않았습니다. 운영 환경만 존재하는 상황에서, 변경사항을 테스트하려면 매번 행정실에 재시도를 요청해야 했습니다.
3. 해결 과정
Step 1: 초기 가설 수립
로그에서 확인할 수 있었던 것은 다음 정보뿐이었습니다.
발생 시간: 2026-03-11 13:06~13:09
클라이언트 IP: 115.145.180.***
HTTP 메서드: POST
대상 엔드포인트: /cssys/work/admin/student/8805
상태 코드: 500
에러 메시지: Response timeout
POST 요청에서 500 에러가 발생했고, 행정실에서 전달받은 파일은 139kB에 불과했으므로 파일 크기 문제는 아니었습니다. 마이그레이션 과정에서 MinIO 연결 설정에 문제가 있을 수 있다는 가설을 세웠지만, 로그 정보만으로는 원인을 특정할 수 없었습니다.
Step 2: 로깅 추가 및 배포
원인 추적을 위해 에러 핸들러에 스택 트레이스 출력을 추가하는 것을 최우선 조치로 결정했습니다.
// 변경 후
console.error(`[${now}] [ERROR] ${req.method} ${req.originalUrl}\\n${err.stack}`);
단 한 줄의 수정이었지만, 이것이 이후 문제 해결의 결정적인 전환점이 되었습니다. PR을 생성하고 머지한 뒤, CI/CD 파이프라인을 통해 자동 배포되었습니다. 이후 행정실에 동일한 동작의 재시도를 요청하는 메일을 발송했습니다.
Step 3: 1차 원인 특정 — multer 필드명 불일치
행정실에서 재시도한 후 로그를 확인하니, 드디어 스택 트레이스가 남아 있었습니다.
MulterError: Unexpected field
at wrappedFileFilter (multer/index.js:40:19)
...
MulterError: Unexpected field. multer가 예상하지 못한 필드명으로 파일이 전송되었다는 의미입니다. 코드를 확인한 결과, 원인은 어드민 뷰의 폼 필드명과 multer 설정의 불일치였습니다.
// 라우터 설정 (admin.js)
router.post('/student/:id', upload.single('upload'), ...)
// multer는 'upload'라는 이름의 파일 필드만 허용
<!-- 어드민 뷰 (student_view.ejs) -->
<input type="file" name="oath"> <!-- 실제 파일 필드: 'oath' -->
<input type="hidden" name="upload" value="oath"> <!-- 파일 타입 정보: 텍스트 -->
multer는 upload라는 이름의 파일 필드를 기다리고 있었지만, 실제 폼에서는 oath라는 이름으로 파일이 전송되고 있었습니다. name="upload"인 필드는 파일이 아닌 hidden 텍스트 필드로, 어떤 종류의 파일인지를 서버에 알려주는 역할이었습니다.
Step 4: 수정 및 2차 에러 발견
수정 방향은 어드민 뷰의 7개 파일 input 필드명을 upload으로 통일하는 것으로 결정했습니다. (수정 PR)
<!-- 수정 전 -->
<input type="file" name="oath">
<!-- 수정 후 -->
<input type="file" name="upload">
수정 후 배포하고 행정실에 다시 테스트를 요청했지만, 여전히 에러가 발생한다는 회신이 돌아왔습니다. 로그를 확인하니 이번에는 다른 에러가 기록되어 있었습니다.
SequelizeValidationError: notNull Violation: StudentFile.time cannot be null,
notNull Violation: StudentFile.ip cannot be null
파일 업로드 기록을 DB에 저장할 때 time과 ip 값이 null로 들어가 유효성 검사에 실패한 것이었습니다. 마이그레이션 과정에서 기존 time/ip 파서의 호환성이 깨진 문제로, 마이그레이션 담당자인 현준 님이 이미 인지하고 있던 이슈였기에 곧바로 수정해 주셨습니다.
스택 트레이스 로깅이 남아 있었기 때문에, 2차 에러의 원인 파악에는 1분도 걸리지 않았습니다.
Step 5: 해결
2차 에러까지 수정하고 배포를 완료했습니다. 행정실 담당자분의 퇴근 시간 이후였기 때문에 다음 날 최종 확인을 받을 수 있었습니다. 에러 보고를 받은 오후 1시부터 수정 배포 완료까지 약 5시간이 소요되었습니다.
4. Claude Code 활용
이번 트러블슈팅 과정에서 Claude Code(Anthropic의 CLI 기반 AI 코딩 도구)를 적극적으로 활용했습니다. 다만, AI에 완전히 의존하지 않고 판단의 주체는 사람이 가져가는 방식으로 사용했습니다.
활용 방식
- 가설 수립 단계: 에러 현상, 환경 변경사항, 제약조건 등의 컨텍스트를 제공하고 원인 가설을 요청했습니다. AI는 MinIO SDK의 HTTPS 설정 불일치, K8s 네트워크 문제, 에러 핸들링 누락 등 여러 가설을 제시했습니다.
- 가설 검증 단계: AI의 가설에 대해 "다른 서비스에서는 MinIO 연결에 문제가 없다"는 사실을 제공하여 반박하고, AI가 재분석하도록 유도했습니다. AI는 응답 시간 4.514ms라는 단서에 주목하여 MinIO hang이 아닌 즉시 실패라는 결론을 도출했습니다.
- 수정 방향 결정 단계: AI는 upload.any()로 서버를 변경하는 방안을 제시했지만, 폼 필드명을 일치시키는 방향이 더 적절하다고 판단하여 직접 방향을 수정했습니다. 학생/어드민 구조 차이를 AI에게 분석시킨 뒤, 최종 수정 범위를 확정했습니다.
- 실행 단계: PR 생성, 커밋 메시지 작성, 코드 수정 등의 실행은 AI에 위임했습니다.
패턴 정리
AI 가설 제시 → 사람이 검증/반박 → AI 재분석 → 사람이 방향 결정 → AI 실행
AI를 "만능 해결사"가 아닌 "빠른 분석 도구"로 활용한 것이 효과적이었습니다. 특히 익숙하지 않은 코드베이스(Node.js/Express)를 빠르게 파악하는 데 큰 도움이 되었습니다.


AI 도구를 운영 서비스에 적용하면서 느낀 점은 개인 블로그의 별도 글로 정리했습니다.
5. 느낀 점
로깅의 중요성
이번 경험에서 가장 크게 느낀 것은 로깅의 힘입니다.
에러 핸들러에 err.stack 한 줄을 추가한 것이 문제 해결의 전환점이었습니다. 실제 슬랙 타임스탬프를 기반으로 MTTR(Mean Time To Repair, 장애 복구 평균 시간)을 비교해 보면 그 차이가 극명합니다.
1차 에러 (로깅 없는 상태) — MTTR: 약 3시간 32분
에러 보고(13:11) → 로깅 추가 배포(14:48) → 행정실 재시도(15:54) → 원인 특정(16:03).
로그에 URL만 남아 있었기 때문에 원인을 추정할 수 없었고, 로깅 코드를 추가하고 배포한 뒤 행정실에 재시도를 요청하는 과정을 거쳐야 했습니다.
2차 에러 (로깅 있는 상태) — MTTR: 약 0분
에러 로그 확인(17:22) → 원인 특정(17:22).
스택 트레이스에 SequelizeValidationError: notNull Violation: StudentFile.time cannot be null이 그대로 찍혀 있었기 때문에, 로그를 본 즉시 원인이 파악되었습니다.

당시 슬랙 대화를 보면 이 속도감이 잘 드러납니다.
행정실 [오후 5:21] 에러 메일 발신
노주희 [오후 5:22] 에러 로그 확인
양현준 [오후 5:22] 원인 파악 완료
같은 시스템, 같은 팀, 같은 날에 발생한 에러인데, 로그 한 줄의 유무가 원인 파악 시간을 3시간에서 0분으로 바꿔놓았습니다.
만약 로깅이 없었다면 어떻게 했을까요? 로컬 환경을 구성해서 디버거를 돌리거나, 추정되는 지점마다 console.log를 찍어가며 하나씩 배포했어야 했을 것입니다. 로그가 얼마나 많은 시간과 노력을 절약해 주는지 체감한 경험이었습니다.
테스트 환경의 부재가 만드는 비효율
cssys는 운영 환경만 존재합니다. 개발 서버가 따로 없고, 어드민 페이지는 보안상 개발자가 직접 접근하기 어렵습니다.
이로 인해 변경사항을 테스트하려면 매번 행정실 담당자에게 메일로 재시도를 요청해야 했습니다. 에러 보고부터 해결까지 주고받은 메일이 상당했는데, 개발 서버가 있었다면 그중 대부분은 필요하지 않았을 것입니다.
개발 서버가 있으면 단순히 테스트뿐 아니라, 대량 데이터를 넣고 부하 테스트를 수행하는 등 학습 목적으로도 활용할 수 있습니다. 장기적으로 보면 개발 서버 구축은 조직 전체의 효율에 기여하는 투자입니다.
SCG가 나아갔으면 하는 방향
이번 에러 대응을 거치면서, 올해 맡게 된 31기 신입 백엔드 스터디의 방향에 대한 확신이 생겼습니다.
SCG는 기술적으로 계속 성장하고 있고, 훌륭한 회원들이 많은 단체입니다. 다만 학생 단체의 특성상, 개발 당시에는 기능을 빠르게 완성하는 데 집중하게 되고, 그것을 만든 사람들은 얼마 지나지 않아 졸업합니다. 과거의 코드를 보면, 미래에 유지보수를 담당할 사람에 대한 고려가 부족한 경우가 많습니다.
이 말을 하고 있는 저 역시 다르지 않았습니다. 아는 것이 없었으니 아는 만큼 구현하고 끝냈습니다. 그런데 지난 1년간 우아한테크코스를 거치면서 좋은 코드에 대한 이야기를 반복적으로 나누었고, 그 경험이 시야를 넓혀주었습니다. 이렇게 얻은 것을 다시 SCG에 전파하고 싶습니다.
한 번에 모든 것을 바꾸기는 어렵습니다. 그래서 31기 신입 스터디부터 시작해보려 합니다.
이번 경험을 거치고 나서, 다음과 같은 문화가 SCG에 정착되었으면 합니다.
- 테스트 코드를 짜는 것을 당연하게 생각한다.
- 로깅을 어떻게 남기는 것이 좋을지 고민한다.
- 유지보수하기 좋은 코드에 대해 고민한다.
- 개발 컨벤션에 대한 논의를 거쳐 팀 전체가 같은 컨벤션을 지킨다.
- 내일의 나와 미래의 후배를 위해 문서로 기록한다.
6. 마무리
8년 된 코드 위에 새로운 인프라를 올리고, 로그 한 줄 없는 상태에서 에러를 추적하고, 행정실과 메일을 주고받으며 문제를 좁혀나간 하루였습니다. 돌이켜보면 기술적으로 어려운 문제는 아니었습니다. multer 필드명 불일치라는, 코드를 보면 바로 알 수 있는 버그였습니다.
문제는 "코드를 볼 수 있는 상태"에 도달하기까지의 과정이었습니다. 로그가 없었고, 테스트 환경이 없었고, 관리자 페이지에 접근할 수 없었습니다. 결국 err.stack 한 줄을 추가하는 것이 이 모든 장벽을 뚫는 열쇠가 되었습니다.
코드는 작성하는 시간보다 유지보수하는 시간이 훨씬 깁니다. 오늘 한 줄의 로그가, 내일 누군가의 5시간을 30분으로 줄여줄 수 있습니다.