DFS 조직도 전수 탐색으로 2GB OOM 발생 → 방문 체크 + 요구 범위 축소로 60MB까지 줄인 사례
TL;DR
- 문제: 자산 서버에서 자산 목록 조회 시 OutOfMemoryError(OOM) 발생
- 원인: 자산 조회를 위해 모든 직원/조직의 하위 직원·하위 조직을 DFS 유사 로직으로 전수 탐색하면서, 탐색 결과를 대량으로 메모리에 적재
- 메서드 1회 실행에 약 2GB 메모리 점유 확인
- 해결
- 이미 조회한 직원/조직은 skip(방문 체크)
- 전 직원/전 조직 조직도 전체를 만들지 않고, 비즈니스 로직에서 실제로 필요한 직원/조직만 탐색/조회
- 결과: 메모리 사용량 2GB → 60MB로 감소, OOM 해결
배경: “자산 목록 조회”에서 왜 조직도가 필요했나?
자산 서버에서 자산 목록을 조회할 때, 단순히 자산 테이블만 보는 것이 아니라 보통 아래와 같은 권한/범위 계산이 필요합니다.
- 특정 사용자가 조회 가능한 자산 범위 = 본인 + 하위 조직/하위 직원 소유 자산
- 혹은 조직 단위 필터링을 위해 조직 트리의 하위 노드 전체가 필요
이 과정에서 “조직도(트리)”를 따라 내려가며 하위 조직/직원을 수집하는 로직이 들어가게 됩니다.
문제 현상: 자산 조회 요청에서 OOM 발생
운영 중 자산 목록 조회 API에서 간헐적으로 장애가 발생했고, 확인 결과 JVM에서 아래와 같은 패턴이 보였습니다.
- 요청 처리 중 메모리 사용량이 비정상적으로 상승
- GC가 과도하게 발생하거나, 결국 OutOfMemoryError로 프로세스가 비정상 종료
원인 분석: DFS 유사 전수 탐색 + 결과 적재가 메모리를 터뜨렸다
조사 결과, 자산 목록을 가져오기 위해 아래 방식이 사용되고 있었습니다.
- 모든 직원 및 조직에 대해,
- 각 노드(직원/조직) 기준으로 하위 직원/하위 조직을 DFS 유사 탐색하며,
- 탐색 결과(하위 집합)를 메모리에 저장
이 방식은 데이터가 커질수록 다음 문제가 겹치며 급격히 악화됩니다.
1) 중복 탐색/중복 적재
- 트리 구조에서 동일 노드를 여러 경로로 다시 방문하거나,
- “A의 하위 집합”을 구할 때 이미 다른 곳에서 계산한 노드를 또 계산/저장하면
- 동일 데이터가 여러 번 메모리에 쌓일 수 있습니다.
2) 전수 조직도 구성 자체가 “요구 범위를 초과”
- 실제 비즈니스 로직은 “이번 요청에서 필요한 범위”만 있으면 되는데,
- 구현은 “전 직원/전 조직의 전체 조직도(혹은 하위 집합 맵)”를 구성하고 있어
- 필요 이상의 데이터를 매번 생성하는 구조였습니다.
3) 데이터 품질 이슈(사이클/비정상 연결) 가능성
조직도 데이터는 원칙적으로 트리/포레스트여야 하지만, 현실에서는 잘못된 데이터로 인해 “사이클”이 생길 가능성도 있습니다.
- 방문 체크가 없다면, 최악의 경우 무한 탐색 또는 폭발적 중복 적재로 이어질 수 있습니다.
재현 및 검증: 스테이징 덤프 + 로컬에서 JVM 메모리 추적
원인을 확실히 보기 위해 아래 절차로 검증했습니다.
- 스테이징 데이터 덤프(직원/조직/관계 데이터 포함)
- 로컬 환경에 데이터 적재 후 동일 조회 요청을 재현
- JVM 메모리 사용량을 추적하여, 특정 메서드 실행 시점에 메모리가 급증하는지 확인
그 결과:
- 문제의 “하위 직원/하위 조직 조회 메서드”가
- 1회 실행에 약 2GB 수준으로 메모리를 점유하는 것을 확인했습니다.
개선 방향: “덜 만들고, 덜 저장하고, 중복은 스킵”
개선은 크게 두 가지 축으로 진행했습니다.
개선 1) 이미 조회한 직원/조직은 Skip (방문 체크)
DFS/BFS 탐색에서 가장 기본적인 안전장치입니다.
visited집합(예:HashSet)을 두고- 이미 방문한 직원/조직 ID는 다시 확장(expand)하지 않도록 처리
이렇게 하면
- 데이터가 크더라도 중복 탐색/중복 적재를 구조적으로 차단할 수 있고,
- 비정상 데이터(사이클)가 있어도 무한 루프를 방지할 수 있습니다.
핵심은
visited.add(id)가false면 이미 방문한 노드이므로 확장을 멈추는 것입니다.
개선 2) “전체 조직도”가 아니라 “현재 필요한 직원/조직만” 조회
두 번째 개선이 더 큰 효과를 냈습니다.
기존 구현은:
- 전 직원/전 조직 기준으로 하위 집합을 구성하거나,
- 조직도를 전수로 구성한 뒤 거기서 필요한 것을 뽑는 방식이었습니다.
개선 후에는:
- 현재 요청의 비즈니스 로직(필터/권한/대상)에 따라
- 정말 필요한 루트(직원/조직)만 선정하고,
- 그 루트에서만 하위 탐색을 수행하도록 변경했습니다.
즉,
- “전수 구축 → 부분 사용”에서
- “부분 구축 → 바로 사용”으로 바꿨습니다.
결과: 2GB → 60MB로 감소, OOM 해결
개선 적용 후 동일 조건에서 측정한 결과는 아래와 같습니다.
- 메모리 사용량: 약 2GB → 약 60MB
- OOM: 재현되지 않음(해결)
이 변화는 단순 최적화를 넘어, 알고리즘/데이터 생성 범위 자체를 재설계한 효과였습니다.
회고: 이런 OOM은 “알고리즘”보다 “범위”에서 터진다
이번 사례에서 인상 깊었던 포인트는 아래입니다.
- DFS/BFS 자체가 문제라기보다,
- 무엇을, 얼마나 만들고, 어디에 저장하느냐가 핵심이었습니다.
- “조직도 전체를 만들고 나서 필요한 걸 쓰자”는 접근은
- 데이터가 커지는 순간 OOM으로 직결될 수 있습니다.
- 방문 체크(visited)는 단순 최적화가 아니라
- 안전장치이자 필수 조건입니다.
마무리
OOM은 “메모리가 부족해서”가 아니라, 대부분 “불필요한 것을 너무 많이 만들고 저장해서” 발생합니다. 이번 케이스도 전수 조직도 구성이라는 과도한 범위가 병목이었고, 방문 체크 + 요구 범위 축소라는 두 가지 원칙만으로도 메모리를 2GB → 60MB까지 낮추며 안정적으로 해결할 수 있었습니다.