페이지 불러오는 중
오랜 기간 서비스된 B2B 솔루션은 비즈니스 환경의 제약으로 인해 최신 인프라나 최신 프레임워크로 즉각적인 전환이 어렵습니다. 이번 프로젝트는 Java 1.5~1.7 기반의 레거시 코드가 혼재된 환경에서 발생한 심각한 메모리 병목 및 OOM(Out of Memory) 현상을 해결한 사례입니다.
StringUtils 등 공통 라이브러리가 부족하던 시절 작성된 커스텀 유틸리티들이 산재.finally 블록의 리소스 close() 누락 및 예외 처리 파편화 발생.new() 및 전역 변수로 관리되어 수거되지 않는 객체 수명주기(Lifecycle) 문제 발생.System.out.println 및 과도한 .toString() 호출로 인한 I/O 블로킹과 임시 객체 남발.개선 작업은 영향도와 우선순위(High/Low)에 따라 단계적으로 진행했습니다.
ThreadLocal 전환 (메모리 폭탄 제거)ThreadLocal 적용: 동일 스레드 내 분할 작업 시 공유가 필요한 객체는 ThreadLocal을 통해 격리하고, 작업 종료 시 반드시 remove()를 호출하여 스레드 풀 재사용 시 메모리 누수를 방지.// ❌ AS-IS: 클래스 멤버/전역 상태로 유지되어 메모리를 계속 점유
public class BatchDataService {
private Map<String, Object> sharedContext = new HashMap<>(); // Memory Bomb 위험
public void process(DataChunk chunk) {
sharedContext.put("data", chunk.getHugePayload()); // 5~10MB 상주
// ...
}
}
// ⭕ TO-BE: 지역 변수 전달 또는 ThreadLocal로 스코프 한정
public class BatchDataService {
private static final ThreadLocal<ExecutionContext> contextHolder = new ThreadLocal<>();
public void process(DataChunk chunk) {
try {
contextHolder.set(new ExecutionContext(chunk.getId()));
executePipeline(chunk.getHugePayload()); // 메서드 인수로 전달하여 라이프사이클 단축
} finally {
contextHolder.remove(); // 풀링된 스레드의 메모리 누수 방지
}
}
}
File Read → Filtering/Shaping → File Write / DB Insert 파이프라인에서 전체 데이터를 한꺼번에 컬렉션에 담아 중간 가공을 수행함으로써 힙 메모리 스파이크 발생.StringBuffer/BufferedReader를 활용한 단위 버퍼 기반 스트리밍 처리.// 파이프라인 스트리밍 및 GC 타이밍 확보
public void processLargeFile(File sourceFile) {
try (BufferedReader reader = new BufferedReader(new FileReader(sourceFile), 8192)) {
String line;
int processedCount = 0;
while ((line = reader.readLine()) != null) {
processLineStreaming(line);
processedCount++;
// Backpressure: 주기적으로 GC 유예 시간 제공 및 힙 부하 완화
if (processedCount % CHUNK_SIZE == 0) {
applyBackpressure();
}
}
}
}
private void applyBackpressure() {
try {
Thread.sleep(10); // 일시 딜레이를 통해 큐 적체 및 메모리 스파이크 완화
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
HikariCP 전환 & 커스텀 라이프사이클 관리)Connection 획득/반환의 생명주기를 강제 제어.✅ 시스템 무중단 & 무변경 패치 완료: DB 스키마나 Java 메이저 버전 변경 없이 마이너 패치로 안정화
✅ OOM 이슈 원천 차단: 5~10MB 단위의 메모리 폭탄 및 리소스 누수 포인트 제거
✅ 비용 절감 및 고객 신뢰도 유지: 계약 기간 내 추가 재개발 비용 없이 안정적인 SLA 유지
✅ 레거시 공통 개선 템플릿 확보: 동일 플랫폼 기반 다른 레거시 프로젝트에 표준 레퍼런스로 활용
단순히 하드웨어 스펙을 증설하거나 메이저 버전을 올리기 어려운 비즈니스 제약 속에서도, 코드 레벨의 스코프 축소, I/O 스트리밍, 그리고 적절한 백프레셔 제어만으로 시스템의 수명을 연장하고 안정성을 확보할 수 있음을 증명한 프로젝트였습니다.
Thread.sleep() 기반 백프레셔의 휴리스틱(임시방편) 한계Thread.sleep(10)을 인가하여 GC가 일할 시간을 벌어주었음.ThreadLocal 도입으로 인한 또 다른 '시한폭탄' 리스크ThreadLocal로 스레드 격리를 수행함.try-finally { remove(); }를 누락하면, Tomcat/WAS의 Worker 스레드 풀 재사용 특성상 스레드 풀 메모리 오염(ThreadLocal Leak)이 발생합니다.