본문 바로가기
Spring

관리자 마케팅 알람 페이지의 조회 지연 및 OOM 문제 해결 후기

by SpearZero 2025. 5. 11.

문제 발생

어느날 모바일 앱에서 여러 API 호출이 일제히 실패하는 현상이 발생했다. 슬랙 알림을 확인해 보니 매니지 서버가 중지되어 있었고, 해당 서버에 접속해 보니 힙 덤프가 생성되어 있었다.

 

매니지 서버는 단순 관리자 페이지용이 아니라, 앱에서 사용하는 휴일 정보, 이벤트 정보, 공지 사항 등 다양한 데이터를 제공하고 있었다. 매니지 서버의 중단은 전체 앱 API 흐름에 큰 영향을 주는 연쇄 장애로 이어졌다. 장애 직전에는 특정 페이지의 응답 속도가 점점 느려지고 있었으며, 이후 과도한 메모리 적재가 누적되면서 OOM이 발생한 것으로 분석되었다.

 

원인 분석

힙 덤프와 스택 트레이스를 분석한 결과, 마케팅 알림 전송 결과 페이지 조회 시 특정 엔티티가 140만 개 이상 메모리에 적재되어 있었고, 이로 인해 OOM(Out of Memory) 오류가 발생한 것으로 확인되었다.

 

문제가 발생한 지점은 마케팅 알람을 가져오면서 해당 마케팅 알람의 총 개수 및 성공 개수를 집계하는 부분이었다. 해당 페이지의 조회 로직은 JPA 기반으로 구성되어 있었으며, DTO 변환 과정에서 entity.getTargets()와 같은 메서드를 호출하면서 수십만 개의 대상 객체가 한 번에 메모리에 적재되는 문제가 발생한 것이었다.

 

문제가 발생한 해당 코드는 다음과 같다.

// 클래스 및 변수명은 임의로 수정했습니다.
public AlarmResultDto toDto(AlarmEntity entity) {
    return AlarmResultDTO.builder()
        ...중략
        .targetCount(entity에서 대상 목록을 조회하여 size() 호출)
        .successCount(entity에서 대상 중 성공 항목만 필터링하여 집계)
        .build();
}

마케팅 알림은 일반적으로 수천~수만 명 이상의 사용자에게 전송되는 대량 메시지다. 따라서 각 알림마다 대상 목록을 메모리에 모두 적재하게 되면, 한 페이지에 알림 10건만 조회하더라도 수십~수백만 개의 객체가 쌓이는 구조가 된다. 이는 메모리 한계를 초과해 OOM으로 직결될 수밖에 없다.

 

해당 코드는 대량 데이터를 조회할 수 있다는 특성이 충분히 고려되지 않은 채 작성된 것으로 보였다. 연관 데이터를 그대로 조회하는 구조가 유지되었고, 이는 실제 운영 환경에서 OOM 문제로 이어졌다.

 

문제 해결

서비스 중단을 방지하기 위해, 우선적으로 문제 페이지의 접근을 제한하였다. 이후 근본 원인을 해결하기 위해 코드 로직을 수정하고 연관 객체 로딩 방식을 개선하였다.

 

문제는 다음과 같이 해결하였다. 마케팅 알림에 연관된 각 사용자별 전송 결과(성공/실패/예외 등)는 전송 완료 이후 변경되지 않는 특성이 있었기 때문에, 전송이 완료된 시점에 전체 대상 수, 성공 수, 실패 수 등을 집계하여 마케팅 알림 테이블에 새로운 컬럼으로 저장하도록 구조를 변경했다.

 

그 결과, 마케팅 알림 결과 페이지에서는 더 이상 연관된 사용자 데이터를 조회하거나 조인할 필요 없이, 마케팅 알림 테이블의 컬럼만 조회하여 전체/성공/실패 건수를 바로 출력할 수 있게 되었다.

 

이후로는 타임아웃이나 OOM이 발생하지 않았으며, 페이지 응답시간은 대략 247ms, 마케팅 알람 정보 조회 API 응답 시간은 대략 30ms정도로 안정화되었다.