Showing Posts From
메모리
- 03 Dec, 2025
Java 메모리 누수: 10시간을 날리다
Java 메모리 누수: 10시간을 날리다 금요일 오후 3시 슬랙이 울렸다. 운영팀이다. "개발님, 프로덕션 서버 메모리 사용량이 계속 올라가요." 금요일 오후 3시. 퇴근까지 3시간. 배포한 지 일주일 된 기능이다. "재시작하면 괜찮아지나요?" "네, 근데 2일 정도 지나면 다시 80% 넘어가요." 메모리 누수다. 확신했다.일단 재시작 "일단 재시작 한 번 해주세요. 제가 로그 확인해볼게요." 재시작했다. 메모리 사용량이 정상으로 돌아왔다. 시간을 벌었다. 하지만 원인을 찾아야 한다. 일주일 전 배포 내역을 확인했다. 커밋 6개. 파일 23개 변경. "이거 하나하나 다 봐야 하나." 커피를 마셨다. 네 번째다. 금요일 오후 4시. 퇴근까지 2시간. 로그를 뒤졌다 애플리케이션 로그. 특이사항 없다. GC 로그. 여기서 뭔가 보인다. [Full GC 3.2G->3.1G(4G), 2.3 secs] [Full GC 3.3G->3.2G(4G), 2.5 secs] [Full GC 3.5G->3.4G(4G), 2.8 secs]Full GC가 계속 돌아도 메모리가 안 줄어든다. 전형적인 메모리 누수. "문제는 어디서 새는가지." 힙 덤프를 떠야 한다. 운영 서버에서. "혹시 성능 영향 있을까요?" "잠깐일 거예요. 10초 정도?" 거짓말이다. 3GB 힙 덤프는 30초 걸린다. 하지만 말 안 했다. 금요일 오후 5시. 퇴근까지 1시간.힙 덤프 분석 덤프 파일을 받았다. 3.2GB. Eclipse MAT을 켰다. 로딩하는데 5분 걸렸다. "Leak Suspects Report"를 눌렀다. One instance of "java.util.HashMap" loaded by <system class loader> occupies 2.8 GB (87.5%) bytes.HashMap 하나가 2.8GB를 먹고 있다. "뭐야 이게." 해당 HashMap의 참조를 추적했다. CacheManager -> userSessionCache -> HashMap아, 세션 캐시구나. 일주일 전에 추가한 기능이다. 사용자 세션을 캐시에 담아서 조회 속도를 높이는. 근데 이게 왜 2.8GB? 금요일 오후 6시. 퇴근 시간이다. 아내한테 카톡 보냈다. "야근할 것 같아. 저녁 먼저 먹어." "또? 어제도 늦었잖아." "미안. 메모리 누수 잡아야 해." "무슨 말인지 모르겠지만 힘내." 코드를 봤다 해당 CacheManager 클래스를 열었다. @Service public class CacheManager { private Map<String, UserSession> userSessionCache = new HashMap<>(); public void addSession(String userId, UserSession session) { userSessionCache.put(userId, session); } public UserSession getSession(String userId) { return userSessionCache.get(userId); } }세션을 넣는 로직은 있다. 빼는 로직이 없다. "아..." 사용자가 로그아웃하거나 세션이 만료돼도 캐시에서 안 지워진다. 일주일 동안 쌓인 세션이 2.8GB. DAU 5만. 평균 세션 크기가 대충 60KB라고 치면. 5만 × 7일 × 60KB = 21GB. 아니다. 중복 로그인도 있으니까... 계산이 안 맞는다. 어쨌든 지워야 한다. 금요일 오후 7시. 배가 고프다.해결책을 찾았다 세 가지 옵션.세션 만료 시 캐시에서 제거 TTL 기반 자동 삭제 LRU 캐시로 교체1번은 로그아웃 로직 모두 수정해야 한다. 시간 걸린다. 3번은 라이브러리 교체. 테스트 범위가 크다. 2번이 답이다. Guava Cache로 교체. @Service public class CacheManager { private Cache<String, UserSession> userSessionCache = CacheBuilder.newBuilder() .expireAfterAccess(2, TimeUnit.HOURS) .maximumSize(10000) .build(); public void addSession(String userId, UserSession session) { userSessionCache.put(userId, session); } public UserSession getSession(String userId) { return userSessionCache.getIfPresent(userId); } }2시간 동안 접근 없으면 자동 삭제. 최대 1만 개 유지. 코드 수정 완료. 20분 걸렸다. 금요일 오후 8시. 치킨집 마감 시간이 10시다. 테스트 로컬에서 테스트했다. 세션 1만 개 생성. 메모리 확인. 정상. 2시간 대기는 못 한다. 시간을 1분으로 줄여서 테스트. 1분 후 GC 로그 확인. 메모리 회수됐다. "됐다." 개발 서버에 배포했다. 30분 모니터링. 문제없다. QA팀한테 슬랙 보냈다. "내일 아침에 테스트 부탁드려요. 급한 버그 픽스입니다." "네, 확인했습니다." 금요일 오후 9시. 프로덕션 배포 결정. 프로덕션 배포 배포 준비했다. 운영팀한테 연락했다. "10분 뒤 배포합니다. 모니터링 부탁드려요." "네, 대기하고 있겠습니다." Jenkins 빌드 시작. 3분 걸린다. 빌드 성공. 배포 시작. 서버 1대씩 순차 배포. Blue-Green 방식이다. 10분 후 배포 완료. 헬스체크 확인. 정상. 메모리 사용량 확인. 40%에서 유지. "일단 됐다." 하지만 확신은 못 한다. 2일 지나봐야 안다. 금요일 오후 10시. 치킨집 문 닫았다. 모니터링 집에 가지 않았다. 사무실에서 30분마다 메모리 그래프를 확인했다. 10시 30분. 메모리 42%. 11시. 메모리 43%. 11시 30분. 메모리 44%. "올라가는 거 아냐?" 아니다. 트래픽이 증가하는 시간대다. 정상 범위다. 12시. 메모리 45%. 안정됐다. 아내한테 카톡 보냈다. "이제 들어간다." "벌써 12시야. 택시 타." "응." 택시를 탔다. 요금 2만3천원. 금요일 자정. 10시간이 지났다. 토요일 아침 9시에 눈을 떴다. 제일 먼저 한 일. 폰으로 메모리 그래프 확인. 메모리 48%. 안정적이다. 슬랙 확인. 장애 알림 없다. "살았다." 이불을 다시 덮었다. 오후 2시에 일어났다. 월요일 오전 출근했다. 주말 동안 메모리 사용량을 확인했다. 최대 52%. 더 이상 안 올라갔다. 운영팀이 슬랙을 보냈다. "서버 안정적이에요. 감사합니다." "네." 팀 회의 때 공유했다. "금요일에 메모리 누수 있었는데 해결했습니다." "고생하셨어요." 끝이다. 아무도 자세히 안 물어본다. 10시간 날린 건 나만 안다. 배운 것캐시에는 항상 만료 정책이 있어야 한다. HashMap은 메모리 누수의 주범이다. 코드 리뷰 때 캐시 관련 코드는 집중해서 본다. 금요일 오후 3시 장애는 최악이다.그리고 하나 더. 힙 덤프 분석은 5분 안에 끝난다고 생각했다. 30분 걸렸다. 항상 예상의 6배가 걸린다. 일주일 후 CTO가 이메일을 보냈다. "지난주 메모리 이슈 빠르게 해결해줘서 감사합니다." 답장 안 했다. 감사하면 연봉 올려주든가.메모리 누수는 찾는 데 1시간, 고치는 데 20분이다. 나머지 8시간 40분은 불안이다.