
터닝 프로젝트를 1년 동안 유지보수 해오면서 많은 일들이 있었다. 그중에서도 서버 쪽을 보면 이해가 안 가는 포인트들이 몇몇 있었다. 제일 아쉬운 건 사용자나 팀원들한테 "서버 터졌어요"라는 말을 듣는 거였다. 모니터링 시스템 만들자고 계속 얘기했지만 여러 이유로 미뤄졌고, 너무 답답해서 혼자 어찌어찌 만들긴 했는데 이것도 결국 다 돈이라... 다시 설득할 근거가 필요했다.
그래서 이참에 개발 프로세스 전체를 처음부터 다시 짚어보면서, 원인도 찾고 해결하는 과정을 기록해보기로 했다.
이런 문제들을 겪고 있었다.
staging 브랜치에 머지해서 CD를 돌리면 높은 확률로 서버가 죽었다. 결국 자동 배포라기보다, EC2에 직접 들어가서 Docker 이미지를 지우고 다시 실행하는 수동 배포를 하고 있었다. (이게 맞나...)deploy-staging.sh 같은 파일로 EC2 서버 안에 숨어있었다. 하나 고치거나 확인하려면 무조건 SSH로 서버에 접속해서 파일을 열어봐야 하는 번거로움이 있었다.일단 내 로컬 환경부터 차근차근 뜯어보기로 했다.
오랫동안 쌓여있던 자잘한 문제부터 해결했다. QueryDSL 의존성이 꼬여서 빌드도 안 되는 문제를 해결하고, 아무도 안 돌려보던 테스트 코드를 돌려봤다. (지금 생각해보면 팀원들이 테스트 코드를 안 돌려봐서 이런 문제를 몰랐던 것 같다, 테스트 코드 없기도하고,,) 일단 지금은 테스트 코드 고치는 것보다 시급한 게 많아서 빌드만 되게끔 하고 넘어가기로 했다.
개발 초부터 계속 뜨던 Lombok @Builder 경고 메시지도 이번 기회에 @Builder.Default를 붙여서 간단하게 해결했다.
// 맨날 보던 경고 메시지
warning: @Builder will ignore the initializing expression entirely.
private List<Scrap> scrapList = new ArrayList<>();
// 이렇게 간단하게 해결!
@Builder.Default
private List<Scrap> scrapList = new ArrayList<>();
이렇게 정리하고 나니 일단 내 로컬에서는 앱이 잘 돌아가는 걸 확인할 수 있었다.
다음으로 설정 파일들을 분석했는데, 여기서 핵심 문제를 발견했다.
application.yml에 적힌 값이랑 서버 환경 변수 값이 달랐다. 스프링 부트는 환경 변수를 1순위로 읽기 때문에, 실제 서버는 내가 생각한 거랑 전혀 다른 설정으로 돌고 있었다. (예: JWT 만료 시간 yml엔 30일, 서버엔 1시간...)ddl-auto: update: 개발할 땐 편하지만, 이게 운영 서버까지 잘못 건드릴 수 있는 위험한 구조였다.찾아낸 문제들을 해결하기 위해 대대적인 공사를 시작했다.
application.yml에 있던 비밀번호 같은 정보들을 다 ${...} 이런 식으로 바꿨다. 이제 yml 파일은 그냥 껍데기고, 진짜 값은 배포할 때 밖에서 넣어주게 되니 훨씬 안전해졌다..sh 파일을 Github Actions 워크플로우(.yml) 안으로 전부 옮겨왔다. 이제 배포 로직도 깃으로 관리할 수 있고, 더 이상 무섭게 서버에 직접 들어갈 필요가 없어져서 너무 편해졌다.application-dev.yml, application-prod.yml 파일을 만들어서 환경마다 설정을 다르게 할 수 있도록 구조를 싹 바꿨다.설정 문제를 해결하고 나니, 배포가 왜 그렇게 실패했는지 진짜 원인이 보이기 시작했다. 범인은 바로 블루/그린 배포할 때 잠깐 생기는 메모리 부족(OOM) 현상이었다.
docker stats 명령어로 확인해보니, 평소에도 우리 서버는 메모리를 43%나 쓰고 있었다. 블루/그린 배포는 새 버전을 먼저 띄우고 옛날 버전을 끄니까, 아주 잠깐 컨테이너 2개가 동시에 돌아가는 순간이 생긴다.
원래 쓰던 거(43%) + 새로 띄운 거(43%) = 순간 메모리 (86% 이상)
결국 메모리가 부족해지면 리눅스가 스스로를 지키기 위해 프로세스를 강제로 죽여버리는데(OOM Killer), 이게 바로 우리가 겪었던 "서버 다운"의 실체였던 거다.
일단 이번 글에서는 우리가 겪던 문제들을 진단하고, 설정 파일을 뜯어고쳐서 CI/CD 파이프라인을 개선하는 과정까지 이야기해봤다.
하지만 아직 진짜 문제인 배포할 때 메모리가 터지는 문제랑 JWT 때문에 CPU가 터지는 문제는 아직 해결하지 못했다. 다음 글에서는 이 문제들을 어떻게 해결했는지, 그 삽질의 과정을 계속 이야기해보겠다.