본문 바로가기
Case Studies (실무)

DFS 조직도 전수 탐색으로 2GB OOM 발생 → 방문 체크 + 요구 범위 축소로 60MB까지 줄인 사례

TL;DR

  • 문제: 자산 서버에서 자산 목록 조회 시 OutOfMemoryError(OOM) 발생
  • 원인: 자산 조회를 위해 모든 직원/조직의 하위 직원·하위 조직을 DFS 유사 로직으로 전수 탐색하면서, 탐색 결과를 대량으로 메모리에 적재
    • 메서드 1회 실행에 약 2GB 메모리 점유 확인
  • 해결
    1. 이미 조회한 직원/조직은 skip(방문 체크)
    2. 전 직원/전 조직 조직도 전체를 만들지 않고, 비즈니스 로직에서 실제로 필요한 직원/조직만 탐색/조회
  • 결과: 메모리 사용량 2GB → 60MB로 감소, OOM 해결

배경: “자산 목록 조회”에서 왜 조직도가 필요했나?

자산 서버에서 자산 목록을 조회할 때, 단순히 자산 테이블만 보는 것이 아니라 보통 아래와 같은 권한/범위 계산이 필요합니다.

  • 특정 사용자가 조회 가능한 자산 범위 = 본인 + 하위 조직/하위 직원 소유 자산
  • 혹은 조직 단위 필터링을 위해 조직 트리의 하위 노드 전체가 필요

이 과정에서 “조직도(트리)”를 따라 내려가며 하위 조직/직원을 수집하는 로직이 들어가게 됩니다.


문제 현상: 자산 조회 요청에서 OOM 발생

운영 중 자산 목록 조회 API에서 간헐적으로 장애가 발생했고, 확인 결과 JVM에서 아래와 같은 패턴이 보였습니다.

  • 요청 처리 중 메모리 사용량이 비정상적으로 상승
  • GC가 과도하게 발생하거나, 결국 OutOfMemoryError로 프로세스가 비정상 종료

원인 분석: DFS 유사 전수 탐색 + 결과 적재가 메모리를 터뜨렸다

조사 결과, 자산 목록을 가져오기 위해 아래 방식이 사용되고 있었습니다.

  • 모든 직원 및 조직에 대해,
  • 각 노드(직원/조직) 기준으로 하위 직원/하위 조직을 DFS 유사 탐색하며,
  • 탐색 결과(하위 집합)를 메모리에 저장

이 방식은 데이터가 커질수록 다음 문제가 겹치며 급격히 악화됩니다.

1) 중복 탐색/중복 적재

  • 트리 구조에서 동일 노드를 여러 경로로 다시 방문하거나,
  • “A의 하위 집합”을 구할 때 이미 다른 곳에서 계산한 노드를 또 계산/저장하면
  • 동일 데이터가 여러 번 메모리에 쌓일 수 있습니다.

2) 전수 조직도 구성 자체가 “요구 범위를 초과”

  • 실제 비즈니스 로직은 “이번 요청에서 필요한 범위”만 있으면 되는데,
  • 구현은 “전 직원/전 조직의 전체 조직도(혹은 하위 집합 맵)”를 구성하고 있어
  • 필요 이상의 데이터를 매번 생성하는 구조였습니다.

3) 데이터 품질 이슈(사이클/비정상 연결) 가능성

조직도 데이터는 원칙적으로 트리/포레스트여야 하지만, 현실에서는 잘못된 데이터로 인해 “사이클”이 생길 가능성도 있습니다.

  • 방문 체크가 없다면, 최악의 경우 무한 탐색 또는 폭발적 중복 적재로 이어질 수 있습니다.

재현 및 검증: 스테이징 덤프 + 로컬에서 JVM 메모리 추적

원인을 확실히 보기 위해 아래 절차로 검증했습니다.

  1. 스테이징 데이터 덤프(직원/조직/관계 데이터 포함)
  2. 로컬 환경에 데이터 적재 후 동일 조회 요청을 재현
  3. 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까지 낮추며 안정적으로 해결할 수 있었습니다.