4화: 야간 슬로우 쿼리는 다 자바 때문입니다
어제 사내 보안 규정이라는 관료제의 두꺼운 벽에 가로막혀 넥서스(Nexus) 업로드가 차단된 직후, 나는 곧바로 인프라운영팀 포털 시스템에 접속하여 '외부 바이너리 사내 반입 및 넥서스 등록 신청서'를 공식 접수했다. 승인 심사까지 최소 일주일이 걸린다는 안내 문구가 화면 상단에 아쉽게 떠 있었지만, 제대로 된 수치화 정적 분석 리포트만 산출해 낸다면 이 고루하고 비효율적인 자바(Java) 8 아키텍처의 한계를 명백히 입증할 수 있으리라 확신했다.
그러나 내가 느긋하게 신청서 승인 순서를 기다릴 여유도 없이, 다음 날 아침 출근하자마자 플랫폼 1팀 사무실 전체에 심상치 않은 정적과 긴장감이 감돌고 있었다. 팀룸의 조명은 평소보다 유독 어둡게 느껴졌고, 개발자들의 책상 위 모니터 화면마다 빨간색 오라클 DB 에러 콘솔과 젠킨스(Jenkins) 경고 로그가 가득했다. 어제저녁 퇴근할 때만 해도 평온했던 사무실이 순식간에 긴박한 비상 장애 대응 본부로 전환된 상태였다. 다들 한 손에 식어버린 믹스커피 잔을 든 채 심각한 표정으로 모니터 스택 트레이스를 응시하고 있었다.
"어제 야간 배치(Batch) 작업 중에 심각한 슬로우 쿼리(Slow Query) 장애가 터졌다면서요?"
내가 출근 가방을 내리며 묻자, 밤샘 탓에 눈이 충혈된 한지훈 선배가 피곤한 기색으로 고개를 깊게 끄덕였다. 어제저녁 늦게 시작된 한빛상사 재고 수불 집계 배치가 새벽녘까지 멈춰 서는 바람에, 시스템 수동 복구와 로그 추적 작업을 진행하느라 밤을 꼴딱 지새운 모양이었다. 턱 밑까지 검게 내려온 다크서클과 푸석푸석한 한 선배의 얼굴에서 야간 배치 장애가 얼마나 혹독하고 지루했는지 짐작할 수 있었다.
"네, 강현호 씨. 새벽 2시경에 한빛상사 재고 수불 배치 작업이 돌다가 특정 SQL 처리 시간이 평소의 10배 이상 치솟으면서 전체 배치 파이프라인이 멈춰 섰습니다. DB 타임아웃이 터지면서 세션들이 연쇄적으로 락(Lock)에 걸렸고, 우선 수동으로 해당 배치 프로세스를 강제 종료(Kill)하고 긴급 경보를 꺼둔 상태입니다."
한 선배의 짧은 브리핑에 내 머릿속에는 즉각적이고 명쾌한 결론이 내려졌다.
'역시 자바 8 생태계와 레거시 런타임의 구조적 한계다.'
수십, 수백 밀리초 단위로 힙(Heap) 메모리를 난잡하게 집어삼키는 레거시 JVM 구조. 거기에 거대한 DTO 객체를 수시로 생성하고 버리는 자바의 런타임 특성상, 대용량 배치 연산 과정에서 가비지 컬렉션(GC) 지연이나 힙 파편화가 터져 DB 커넥션 풀을 제때 반납하지 못했을 것이 100% 확실했다. Rust처럼 메모리를 컴파일 타임에 소유권(Ownership) 규칙으로 철저히 제어했다면 GC 오버헤드 없이 깔끔하고 안정적으로 처리되었을 작업이, 늙은 JVM 런타임의 덜컹거림 때문에 또다시 멈춰버린 것이었다.
"이건 전형적인 자바 런타임과 레거시 프레임워크의 구조적 병목입니다, 선배님. 힙 메모리 파편화나 GC pause가 발생하면서 DB 커넥션 반납이 지연되었고, 결국 연쇄 타임아웃으로 이어졌을 게 확실합니다."
내가 내적 오만을 숨기지 않고 덤덤하게 내뱉자, 옆에서 모니터의 MyBatis XML과 애플리케이션 스택 트레이스를 살피던 박정수 팀장님이 고개를 돌려 나를 바라보았다.
"강현호 씨. 원인을 단정하기 전에 로그와 쿼리 매퍼부터 제대로 훑어보았습니까?"
"네, 팀장님. 출근하자마자 젠킨스 야간 잡 콘솔과 MyBatis 매퍼 XML을 훑어보았습니다. 최신 ORM처럼 객체 관계를 효율적으로 래핑하지도 못하고, 수동으로 짠 쌩 SQL 문장을 XML 파일에 수십 줄씩 나열해 뒀더군요. 가독성도 나쁜 데다 런타임에 동적 SQL을 매번 문자열로 이어 붙이니 자바 애플리케이션 영역에서 파싱 오버헤드가 누적될 수밖에 없습니다."
나는 모니터 화면에 떠 있는 MyBatis XML 파일의 복잡한 dynamic SQL 태그 구문들을 손가락으로 가리키며 내 주장을 이어나갔다.
"결국 자바 8 생태계와 레거시 프레임워크의 한계입니다. DB 단이 아니라 자바 애플리케이션 영역에서 객체 변환과 GC 오버헤드를 견디지 못하고 터진 겁니다. 이런 구조에서는 아무리 SQL을 다듬는다고 해도 언어적 한계를 극복할 수 없습니다."
박 팀장님은 턱을 괴고 잠시 생각에 잠기더니, 이내 내 주장에 확답을 주지 않은 채 사내 전화기를 들었다.
"임동현 PM님, 새벽기술 박정수입니다. 어제 야간 배치 슬로우 쿼리 관련해서 고객사 DBA 쪽에 오라클 DB AWR(Automatic Workload Repository) 리포트나 쿼리 세션 로그를 좀 요청받을 수 있겠습니까? 애플리케이션 로그만으로는 정확한 세션 락 상태를 파악하기가 어려워서요."
전화기 스피커 너머로 한빛상사 임동현 PM의 건조하고 칼 같은 목소리가 흘러나왔다.
"박 팀장님. 운영 오라클 DB 접근이나 락 세션 분석은 고객사 DBA 전용 권한입니다. 사전 승인된 변경 창구(Change Window) 외에는 내부 DBA가 아닌 외주 SI 팀에 AWR 덤프나 DB 내부 통계 로그를 직접 넘겨줄 수 없습니다. 개발 환경에서 비슷하게 재현해 보시고, 정말 DB 쪽 문제라는 명확한 근거가 없으면 운영 DB 세션 요청은 어렵습니다."
고객사의 단호한 거절에 박 팀장님은 씁쓸한 표정으로 전화기를 내려놓았다. 개발자가 실제 문제가 터진 운영 DB의 락 상태나 세션 정보를 직접 열어볼 수조차 없는 답답한 폐쇄망 보안 환경이었다.
그때, 밤새 로그를 훑어보던 한지훈 선배가 무심하게 키보드를 두드리며 한마디를 건넸다.
"강현호 씨, 자바 GC 문제라고 단정하기 전에... 오라클 실행 계획(Execution Plan)이나 풀 스캔(Full Scan) 여부부터 먼저 확인해 봐야 하지 않을까요? 인덱스를 타는지 타지 않는지에 따라 쿼리 응답 속도는 수천 배 이상 차이 나니까요."
한 선배의 말은 지극히 기본적인 지적이었다. 하지만 나는 내심 고개를 저었다.
'선배님은 자바 8 레거시가 지닌 런타임 불확실성을 너무 경시하고 계셔. 인덱스 한두 개 타는 문제 이전에, 수많은 DTO 객체를 만들고 털어내는 자바 영역의 가비지 컬렉터와 동적 매퍼 파싱이 원흉인 것은 명백하다. 오라클 실행 계획이야 자바에서 매개변수를 넘기는 과정에서 병목이 생겨 느려진 결과일 뿐이지.'
나는 겉으로는 예의 바르게 대답했다.
"네, 선배님 말씀대로 DB 인덱스 충돌 가능성도 배제할 수는 없겠지요. 하지만 런타임 객체 지연이 1차적 원인이라는 제 생각에는 변함이 없습니다."
박 팀장님은 팀원들을 둘러보며 정리하듯 말했다.
"어쨌든 운영 DB 접근이 막혀 있으니, 우선은 개발 DB에서 야간 배치와 동일한 대용량 테스트 데이터를 밀어 넣고 재현 시도를 해봅시다. 원인이 명확히 밝혀지기 전까지는 섣불리 소스나 설정을 건드리지 마세요."
나는 자리로 돌아와 내 사내 노트북 화면의 인프라팀 승인 포털 페이지를 다시 확인했다. 접수된 신청서는 '보안 스캔 대기 중' 상태로 떠 있었다.
'두고 봐라. 일주일 뒤 내 Rust 정적 분석 도구 반입 승인이 떨어지고 젠킨스에 연동되는 순간, 이 지긋지긋한 자바 레거시 프레임워크의 의존성 구조와 런타임 한계가 얼마나 심각한지 수치로 깔끔하게 증명해 낼 테니까. 그때가 되면 다들 내 말이 맞았다는 걸 인정할 수밖에 없을 것이다.'
나는 자바 생태계에 대한 깊은 불신과 내적 오만을 그대로 품은 채, 개발 환경에서의 배치 재현 테스트를 준비하기 시작했다.
<5화에서 계속>
흩어진 경험을 하나의 이야기로, 슈퍼점프와 함께 합격으로 가는 자기소개서를 빠르게 완성하세요!! 로그인 후 ‘내 정보 > 쿠폰’에서 SUPERJUMP를 입력하고 일주일간 무료로 체험해 보세요!
https://share.google/9Pe7tyMW8QqtqnLpj