메모리와 실행 모델 (Memory & Execution Model)

목차


메모리와 실행 모델을 왜 묻나

백엔드 면접에서 메모리와 실행 모델을 묻는 이유는 언어 문법을 얼마나 외웠는지 보려는 것이 아닙니다.

보통은 다음을 확인합니다.

  • 성능 감각: 느린 원인이 CPU인지, I/O인지, 메모리인지 구분할 수 있는가
  • 안정성 감각: 메모리 사용량 증가, 객체 누수, 장시간 실행 문제를 이해하는가
  • 런타임 이해: 스레드, 이벤트 루프, 가비지 컬렉션이 성능에 어떤 영향을 주는가
  • 문제 해결 방식: 추측으로 튜닝하지 않고 프로파일링으로 병목을 찾는가

즉, 이 주제의 핵심은 “메모리 구조를 외운다”가 아니라
실행 중인 프로그램이 어떤 비용으로 동작하는지 설명할 수 있는가입니다.

관련된 동시성 흐름은 동시성 (Concurrency), Python 런타임 관점은 Python 문서와 같이 보면 좋습니다.


스택과 힙

스택(Stack)과 힙(Heap)은 프로그램 실행 시 메모리를 바라보는 가장 기본적인 구분입니다.

영역 주 용도 특징
스택 함수 호출 프레임, 지역 변수, 반환 주소 빠르고 수명 관리가 단순함
동적으로 생성된 객체, 공유 데이터 유연하지만 관리 비용이 큼

스택

스택은 함수 호출이 쌓였다가 빠지는 구조로 동작합니다.

  • 장점: 할당과 해제가 빠릅니다
  • 장점: 함수 범위가 끝나면 수명이 비교적 명확합니다
  • 주의점: 너무 깊은 재귀나 큰 지역 변수는 스택 사용량을 키울 수 있습니다

힙은 실행 중 필요한 크기의 객체를 동적으로 저장하는 영역입니다.

  • 장점: 크기와 수명이 다양한 객체를 유연하게 다룰 수 있습니다
  • 주의점: 객체 수가 많아질수록 추적 비용과 메모리 단편화 문제가 커질 수 있습니다
  • 주의점: 참조가 오래 남으면 예상보다 메모리를 오래 점유할 수 있습니다

면접에서는 “스택은 빠르고 힙은 느리다“처럼 단정하기보다,
수명 관리와 접근 패턴이 다르다는 식으로 설명하는 편이 좋습니다.


가비지 컬렉션

가비지 컬렉션(Garbage Collection, GC)은 더 이상 사용하지 않는 객체를 회수하는 런타임 메커니즘입니다.

GC가 필요한 이유는 단순합니다. 개발자가 모든 객체 수명을 직접 관리하면 생산성이 크게 떨어지고 실수가 늘어나기 때문입니다.

GC를 설명할 때 핵심은 다음입니다.

  • 장점: 메모리 해제 실수를 줄이고 생산성을 높입니다
  • 장점: 복잡한 객체 그래프를 자동으로 관리할 수 있습니다
  • 단점: 수집 시점과 비용을 런타임이 결정하므로 지연이 생길 수 있습니다
  • 단점: 객체를 너무 많이 만들면 GC 부담이 커집니다

언어마다 GC 방식은 다르지만 공통적으로 중요한 면접 포인트는 같습니다.

  • 객체를 짧게 많이 만드는 코드가 어떤 부담을 만드는가
  • GC가 자동이라고 해서 메모리 문제가 사라지는 것은 아닌가
  • pause time, throughput, 메모리 사용량 사이에 어떤 균형이 필요한가

즉, GC는 “메모리를 공짜로 관리해 주는 기능”이 아니라
개발 편의성과 런타임 비용을 교환하는 메커니즘입니다.


객체 생명주기와 메모리 누수

메모리 누수(Memory Leak)는 C나 C++처럼 해제를 안 해서만 생기는 문제가 아닙니다. GC 언어에서도 참조가 계속 남아 있으면 객체는 회수되지 않습니다.

백엔드 서비스에서 흔한 패턴은 다음과 같습니다.

  • 전역 캐시의 무한 성장: eviction 정책 없이 계속 쌓이는 경우
  • 이벤트 핸들러/콜백 누락: 해제하지 않은 참조가 남는 경우
  • 세션/커넥션 관리 실수: 종료되어야 할 객체가 오래 유지되는 경우
  • 큰 객체 보관: 로그, 응답, 배치 결과를 메모리에 오래 잡아 두는 경우

객체 생명주기를 설명할 때는 다음 흐름이 중요합니다.

  1. 객체가 언제 생성되는가
  2. 누가 그 객체를 참조하는가
  3. 언제 참조가 끊겨야 하는가
  4. 그 시점이 실제로 지켜지는가

좋은 답변은 “메모리 누수는 위험하다”가 아니라,
오래 사는 객체와 짧게 살아야 하는 객체가 섞였을 때 어떤 문제가 생기는지를 설명하는 답변입니다.


런타임과 성능 프로파일링

성능 문제를 다룰 때 가장 중요한 태도는 추측보다 측정입니다.

프로파일링(Profiling)은 프로그램이 어디에서 시간을 쓰고, 어디에서 메모리를 쓰는지 측정하는 과정입니다.

보통 다음 질문으로 정리할 수 있습니다.

  • CPU 시간을 가장 많이 쓰는 함수는 무엇인가
  • 할당이 많이 일어나는 지점은 어디인가
  • 요청 지연의 대부분이 외부 I/O 대기 때문인가
  • 특정 배포 이후 메모리 사용량이 계속 증가하는가

대표적인 프로파일링 관점은 다음과 같습니다.

  • CPU 프로파일링: 계산이 몰리는 핫스팟을 찾음
  • 메모리 프로파일링: 객체 수, 할당량, 누수를 찾음
  • 트레이싱/타이밍 측정: 요청 경로에서 어느 단계가 느린지 찾음

면접에서는 “최적화하겠다”보다
먼저 프로파일링으로 병목이 CPU인지, I/O인지, 메모리인지 구분하겠다고 설명하는 편이 더 실무적입니다.


CPU 병목과 I/O 병목

성능 이슈를 말할 때 CPU-bound와 I/O-bound를 구분할 수 있어야 답변이 깔끔해집니다.

구분 특징 주 대응 방식
CPU-bound 계산이 많아 CPU 사용률이 높음 알고리즘 개선, 병렬 처리, 네이티브 확장 검토
I/O-bound 네트워크, 디스크, DB 대기가 큼 비동기 처리, 배치/병렬화, 호출 수 감소

CPU 병목

  • 복잡한 계산, 압축, 암호화, 대용량 직렬화에서 자주 나타납니다
  • CPU 사용률이 높고, 프로파일러에서 특정 함수가 오래 잡힙니다
  • 알고리즘 개선이나 병렬 처리 전략이 중요합니다

I/O 병목

  • DB 호출, 외부 API, 파일 읽기/쓰기, 네트워크 대기가 원인인 경우가 많습니다
  • CPU는 한가한데 응답 시간이 느린 패턴이 자주 나옵니다
  • 비동기 처리, 요청 수 축소, 캐시, 배치가 더 효과적일 수 있습니다

즉, 성능 문제는 하나의 답으로 해결되지 않습니다.
CPU 문제를 비동기로 풀려 하거나, I/O 문제를 무작정 스레드 수만 늘려 해결하려 하면 오히려 구조가 더 나빠질 수 있습니다.


코드 예시

아래 예시는 메모리 사용량이 계속 커지는 전역 캐시 패턴을 단순화한 예시입니다.

cache = {}


def load_user_profile(user_id: int) -> dict:
    if user_id not in cache:
        # 실제 서비스라면 DB나 외부 API에서 가져온다고 가정
        cache[user_id] = {
            "user_id": user_id,
            "preferences": ["email", "sms", "push"] * 1000,
        }

    return cache[user_id]

이 예시의 문제는 다음과 같습니다.

  • cache가 전역으로 유지되어 프로세스 수명과 같이 갑니다
  • 만료 정책이 없어서 요청이 들어올수록 메모리가 계속 증가합니다
  • GC가 있어도 참조가 살아 있으므로 객체를 회수할 수 없습니다

즉, 메모리 문제는 “GC가 있으니 괜찮다”로 끝나지 않고,
객체를 얼마나 오래 붙잡고 있는가까지 같이 봐야 합니다.


면접 포인트

  • 스택과 힙은 속도 비교보다 수명 관리와 용도가 다르다는 점으로 설명하는 편이 좋습니다.
  • GC는 개발 편의성을 높여 주지만, 객체 생성 패턴과 pause 비용이라는 대가가 있습니다.
  • GC 언어에서도 참조가 오래 남으면 메모리 누수가 생길 수 있습니다.
  • 성능 문제는 추측보다 프로파일링으로 CPU, I/O, 메모리 병목을 먼저 구분해야 합니다.
  • 좋은 답변은 객체 생명주기, 메모리 사용량, 런타임 동작을 실제 운영 문제와 연결합니다.

참고 자료