개발 도구

Go 애플리케이션 메모리 누수, pprof만으로 부족했던 심층 진단 직접 해보니

강코의 코딩 일기 2026. 7. 29. 09:23
반응형

Go 애플리케이션에서 발생하는 메모리 누수를 pprof를 넘어선 심층 분석과 커스텀 프로파일링 도구로 해결했던 실제 경험과 점검 항목을 공유합니다.

안녕하세요! 여러분은 Go 언어로 만든 애플리케이션을 개발하면서 "메모리가 왜 이렇게 계속 늘어나지?"라는 고민을 해본 적 있으신가요? 특히 장시간 실행되는 서버 애플리케이션이라면 이 문제가 더욱 심각하게 다가올 겁니다. 저도 비슷한 문제에 부딪혀 며칠 밤낮을 고생한 경험이 있습니다. 처음에는 Go pprof만 믿고 덤볐지만, 생각보다 깊숙이 숨어있는 메모리 누수 범인을 잡는 것이 쉽지 않더군요.

이 글에서는 Go 애플리케이션의 메모리 누수를 진단하고 해결하는 과정을 제가 직접 겪었던 경험을 바탕으로, pprof의 기본적인 사용법부터 시작해서, pprof만으로는 부족했던 부분을 어떻게 파고들었는지, 그리고 커스텀 프로파일링 도구를 만들어 활용하는 방법까지 자세히 공유하려고 합니다. 프로그래밍을 막 시작한 분들도 쉽게 이해할 수 있도록 전문 용어는 최대한 풀어서 설명할 테니, 걱정 말고 따라오세요!

메모리 누수는 마치 우리 집 어딘가에서 물이 새는 것과 같습니다. 처음에는 작은 물방울에 불과하지만, 시간이 지나면 집 전체를 망가뜨릴 수 있죠. 개발에서도 마찬가지입니다. 작은 메모리 누수가 결국 애플리케이션 전체의 성능 저하와 장애로 이어질 수 있습니다. 자, 그럼 함께 메모리 누수 탐정 모자를 쓰고 범인을 찾아 나서 볼까요?

📑 목차

Go 애플리케이션 메모리 누수 진단 및 해결: pprof를 넘어선 심층 분석과 커스텀 프로파일링 도구 활용 - bunnies, three, trio, go, figurines, game, decoration, ornament, outside, custom, easter decorations

Image by _Alicja_ on Pixabay

1. Go 애플리케이션 메모리 누수, 왜 발생하고 왜 위험할까요?

본격적인 진단에 앞서, 메모리 누수가 무엇인지, 그리고 왜 Go 애플리케이션에서 문제가 되는지 간단히 짚고 넘어갈 필요가 있습니다. 컴퓨터 프로그램은 실행될 때 메모리(RAM)라는 공간을 사용합니다. 이 공간에 필요한 데이터나 변수 등을 저장하고 처리하죠. 작업이 끝나면 메모리를 다시 운영체제에 반환해서 다른 프로그램이 사용할 수 있도록 해야 합니다.

메모리 누수(Memory Leak)는 프로그램이 더 이상 필요 없는 메모리를 운영체제에 반환하지 않고 계속 점유하고 있는 현상을 말합니다. 마치 쓰레기를 버리지 않고 계속 집에 쌓아두는 것과 같아요. Go 언어는 가비지 컬렉터(Garbage Collector, GC)라는 똑똑한 친구가 있어서 사용하지 않는 메모리를 자동으로 정리해 줍니다. 하지만 이 가비지 컬렉터도 모든 상황에서 완벽하게 작동하는 것은 아닙니다. 예를 들어, 어딘가에서 이 메모리를 계속 "사용 중"이라고 착각하게 만들면, 가비지 컬렉터는 해당 메모리를 정리하지 않고 계속 남겨두게 됩니다.

1.1 메모리 누수가 위험한 이유

메모리 누수는 당장 눈에 띄지 않지만, 시간이 지남에 따라 다음과 같은 심각한 문제를 일으킬 수 있습니다.

  • 성능 저하: 애플리케이션이 사용 가능한 메모리가 줄어들면서, 새로운 작업을 처리하기 위해 디스크(하드 드라이브)의 가상 메모리(Swap)를 사용하게 됩니다. 디스크는 RAM보다 훨씬 느리기 때문에, 애플리케이션의 응답 속도가 현저히 느려집니다.
  • 애플리케이션 충돌: 메모리가 완전히 고갈되면, 운영체제가 해당 애플리케이션을 강제로 종료하거나, 아예 시스템 전체가 멈출 수도 있습니다. (흔히 OOM, Out Of Memory라고 부릅니다.)
  • 비용 증가: 클라우드 환경에서는 메모리 사용량에 따라 비용이 청구되기도 합니다. 불필요하게 높은 메모리 사용량은 곧 불필요한 비용 지출로 이어집니다.

이런 이유 때문에 메모리 누수는 개발자가 반드시 잡아야 할 중요한 문제입니다.

2. Go pprof, 메모리 누수 진단의 첫걸음

Go 언어 개발자라면 pprof라는 도구를 빼놓고 성능 분석을 이야기할 수 없습니다. pprof는 Go 프로그램의 실행 시간, 메모리 사용량, 고루틴(Goroutine) 상태 등 다양한 성능 지표를 시각적으로 보여주는 강력한 프로파일링 도구입니다. 메모리 누수를 진단할 때도 pprof가 가장 먼저 사용되는 기본적인 도구입니다.

2.1 pprof 활성화 및 기본 사용법

Go 애플리케이션에 pprof를 활성화하는 것은 매우 간단합니다. 일반적으로 HTTP 서버에

net/http/pprof

패키지를 임포트(import)하기만 하면 됩니다.


package main

import (
	"fmt"
	"net/http"
	_ "net/http/pprof" // pprof 임포트

	"time"
)

func main() {
	go func() {
		// 메모리 누수를 유발할 수 있는 예시 함수
		leakMemory()
	}()

	fmt.Println("pprof 서버가 :6060 포트에서 시작되었습니다.")
	http.ListenAndServe(":6060", nil) // pprof 엔드포인트가 이 서버를 통해 노출됨
}

var globalSlice []byte

func leakMemory() {
	ticker := time.NewTicker(time.Second)
	defer ticker.Stop()
	for range ticker.C {
		// 매 초마다 1MB의 메모리를 할당하여 전역 슬라이스에 추가
		// 이 메모리는 가비지 컬렉터에 의해 회수되지 않음
		data := make([]byte, 1024*1024) // 1MB 할당
		globalSlice = append(globalSlice, data...)
		fmt.Printf("현재 globalSlice 크기: %d MB\n", len(globalSlice)/(1024*1024))
	}
}

위 코드에서

_ "net/http/pprof"

처럼 임포트만 해도

/debug/pprof

경로로 pprof 엔드포인트가 노출됩니다. 이제 애플리케이션을 실행하고 웹 브라우저에서

http://localhost:6060/debug/pprof/

에 접속해 보세요. 다양한 프로파일링 데이터를 볼 수 있습니다.

2.2 pprof로 힙 메모리 프로파일링하기 (Heap Profile)

메모리 누수 진단에서 가장 중요한 것은 힙(Heap) 메모리 프로파일입니다. 힙은 프로그램이 동적으로 메모리를 할당하는 공간을 의미합니다. pprof를 통해 현재 힙 메모리 상태를 분석하여 어떤 코드에서 메모리를 많이 사용하고 있는지, 혹은 해제되지 않고 남아있는 메모리가 있는지 확인할 수 있습니다.

애플리케이션을 실행한 상태에서 터미널에서 다음 명령어를 입력하여 힙 프로파일을 가져올 수 있습니다.


go tool pprof http://localhost:6060/debug/pprof/heap

이 명령어를 실행하면 pprof 셸로 진입하게 됩니다. 여기서

top

명령어를 입력하면 현재 메모리를 가장 많이 점유하고 있는 함수들을 순서대로 볼 수 있습니다.

list 함수이름

명령어를 사용하면 해당 함수가 어떤 코드에서 메모리를 할당하는지 소스 코드를 직접 볼 수도 있습니다.

더 시각적인 분석을 위해서는 다음 명령어를 사용합니다.


go tool pprof -http=:8080 http://localhost:6060/debug/pprof/heap

그러면 웹 브라우저에서

http://localhost:8080

으로 접속하여 Flame Graph, Call Graph 등 다양한 시각화된 그래프를 통해 메모리 사용 현황을 한눈에 파악할 수 있습니다. 특히 Flame Graph는 메모리가 어디서 많이 할당되고 있는지 직관적으로 보여주어 범인을 찾는 데 큰 도움을 줍니다.

💡 pprof 점검 항목 💡
  • 힙 프로파일 주기적 확인: 애플리케이션 실행 초기와 시간이 지난 후의 힙 프로파일을 비교하여 메모리 사용량이 계속 증가하는지 확인합니다.
  • top 명령어 활용:
    top
    명령어로 메모리를 가장 많이 사용하는 함수를 식별합니다.
  • Flame Graph/Call Graph 시각화:
    -http
    옵션으로 시각화된 그래프를 보면서 메모리 할당 경로를 추적합니다. 특히 인바운드(Inbound)가 많거나, 오랜 시간 동안 풀리지 않는(Retained) 메모리를 유심히 봅니다.
  • Allocations vs Inuse: pprof에서 allocations는 프로그램 시작 이후 전체 할당된 메모리 양을, inuse_space는 현재 사용 중인 메모리 양을 나타냅니다. 메모리 누수는 주로 inuse_space가 계속 증가하는 형태로 나타납니다.

3. pprof만으로는 부족했던 순간: 숨겨진 메모리 누수 찾기

pprof는 강력한 도구이지만, 제가 겪었던 몇몇 상황에서는 pprof만으로는 메모리 누수의 정확한 원인을 파악하기 어려웠습니다. 특히 다음과 같은 경우에 말이죠.

  • 작은 누수가 장시간에 걸쳐 발생하는 경우: 매번 소량의 메모리가 누수되어 pprof 그래프에서 눈에 띄게 큰 비중을 차지하지 않는 경우.
  • 외부 라이브러리 내부에서 발생하는 누수: 내가 작성한 코드가 아닌, 사용하는 외부 라이브러리 내부에서 메모리를 잘못 관리하는 경우.
  • 특정 조건에서만 발생하는 누수: 특정 요청 패턴이나 특정 데이터가 들어왔을 때만 발생하는 누수.
  • 고루틴 누수(Goroutine Leak): 고루틴이 종료되지 않고 계속 남아있어 해당 고루틴이 점유하는 스택 메모리나 클로저(Closure)가 참조하는 힙 메모리가 해제되지 않는 경우. (엄밀히 말하면 힙 메모리 누수와는 조금 다르지만, 전체 메모리 사용량을 증가시키는 주범이 될 수 있습니다.)

이런 상황에서는 pprof가 보여주는 '어디서 메모리가 할당되었는가' 정보만으로는 '왜 이 메모리가 해제되지 않는가'를 파악하기 어려웠습니다. 그래서 저는 좀 더 깊이 파고들 필요를 느꼈습니다.

3.1 고루틴 누수 진단

Go 애플리케이션에서 고루틴 누수메모리 누수와 함께 흔히 발생하는 문제입니다. 종료되어야 할 고루틴이 어떤 이유로 계속 실행 중이거나, 블록되어(blocked) 있어서 사라지지 않는 경우입니다. 고루틴은 스택 메모리를 사용하며, 고루틴 내부의 클로저가 외부 변수를 참조하면 해당 변수의 힙 메모리도 해제되지 않을 수 있습니다.

pprof의 고루틴 프로파일을 통해 현재 실행 중인 고루틴의 개수와 상태를 확인할 수 있습니다.


go tool pprof http://localhost:6060/debug/pprof/goroutine

이 명령어로

go tool pprof

셸에 진입한 후

top

명령어를 입력하면 가장 많은 고루틴이 생성된 지점을 확인할 수 있습니다. 만약 고루틴의 개수가 계속 증가한다면, 어딘가에서 고루틴이 제대로 종료되지 않고 있는 것입니다.

💡 고루틴 누수 점검 항목 💡
  • 고루틴 프로파일 주기적 확인: 애플리케이션 실행 중 고루틴 개수가 비정상적으로 증가하는지 확인합니다.
  • 채널(Channel) 사용 확인: 채널을 사용하는 고루틴은 채널이 닫히거나 메시지를 받을 고루틴이 없으면 영원히 블록될 수 있습니다.
    select
    문에
    default
    time.After
    를 사용하여 타임아웃 처리를 고려합니다.
  • context.Context 활용: 장시간 실행되는 고루틴은
    context.Context
    를 사용하여 외부에서 취소(cancel) 신호를 보낼 수 있도록 구현해야 합니다.
  • 무한 루프(Infinite Loop) 주의: 명시적인 종료 조건 없이
    for {}
    등으로 무한 루프를 돌고 있는 고루틴이 없는지 확인합니다.
Go 애플리케이션 메모리 누수 진단 및 해결: pprof를 넘어선 심층 분석과 커스텀 프로파일링 도구 활용 - woman, rituals, tradition, bali, indonesia, hindu, culture, custom, lombok, bali, bali, bali, bali, bali, indonesia, hindu, hindu, hindu, lombok

Image by 18531141 on Pixabay

4. 커스텀 프로파일링 도구 활용: 더 깊은 통찰

pprof가 보여주는 데이터는 주로 '어떤 함수가' 메모리를 많이 사용하는지에 집중합니다. 하지만 때로는 '언제', '얼마나 자주', '어떤 객체가' 메모리를 점유하고 해제하지 않는지 실시간으로 추적해야 할 필요가 있습니다. 이때 커스텀 프로파일링 도구가 빛을 발합니다. Go 언어의

runtime

패키지를 활용하면 직접 메모리 사용량을 모니터링하고 로그를 남길 수 있습니다.

4.1 runtime.MemStats를 활용한 주기적인 메모리 스냅샷

Go 언어의

runtime

패키지에는

ReadMemStats

함수가 있습니다. 이 함수는 현재 애플리케이션의 메모리 상태에 대한 다양한 통계 정보를 제공하는

MemStats

구조체를 채워줍니다. 이를 활용하여 주기적으로 메모리 스냅샷을 찍고 로그로 남기면, 메모리 사용량 추이를 정확히 파악할 수 있습니다.


package main

import (
	"fmt"
	"runtime"
	"time"
)

// MemStats 구조체에서 필요한 정보만 추출하여 로깅
type MemorySnapshot struct {
	Timestamp      time.Time
	Alloc          uint64 // 현재 할당된 힙 메모리 (바이트)
	TotalAlloc     uint64 // 프로그램 시작 이후 전체 할당된 힙 메모리 (바이트)
	Sys            uint64 // OS로부터 얻은 전체 메모리 (바이트)
	NumGC          uint32 // 가비지 컬렉션 실행 횟수
	Goroutines     int    // 현재 실행 중인 고루틴 개수
}

func logMemoryStats() {
	var m runtime.MemStats
	runtime.ReadMemStats(&m) // 현재 메모리 통계 읽기

	// 현재 고루틴 개수 가져오기
	goroutines := runtime.NumGoroutine()

	snapshot := MemorySnapshot{
		Timestamp:  time.Now(),
		Alloc:      m.Alloc,
		TotalAlloc: m.TotalAlloc,
		Sys:        m.Sys,
		NumGC:      m.NumGC,
		Goroutines: goroutines,
	}

	// 실제 서비스에서는 로깅 시스템(예: ELK 스택, Prometheus 등)에 전송
	fmt.Printf("[%s] Alloc: %.2f MB, TotalAlloc: %.2f MB, Sys: %.2f MB, NumGC: %d, Goroutines: %d\n",
		snapshot.Timestamp.Format("2006-01-02 15:04:05"),
		float64(snapshot.Alloc)/(1024*1024),
		float64(snapshot.TotalAlloc)/(1024*1024),
		float64(snapshot.Sys)/(1024*1024),
		snapshot.NumGC,
		snapshot.Goroutines,
	)
}

func startMemoryMonitor(interval time.Duration) {
	ticker := time.NewTicker(interval)
	defer ticker.Stop()
	for range ticker.C {
		logMemoryStats()
	}
}

func main() {
	go startMemoryMonitor(5 * time.Second) // 5초마다 메모리 상태 로깅

	// 여기에 실제 애플리케이션 로직...
	// 예시: 메모리 누수를 일으킬 수 있는 작업
	var dataStore [][]byte
	for i := 0; i < 10; i++ {
		time.Sleep(2 * time.Second)
		data := make([]byte, 1024*1024*5) // 5MB 할당
		dataStore = append(dataStore, data) // 계속 추가 (누수 유발)
		fmt.Printf("데이터 스토어에 %d번째 데이터 추가. 총 %d MB\n", i+1, len(dataStore)*5)
	}

	fmt.Println("메인 함수 종료 (실제로는 서버가 계속 실행될 것임)")
	select {} // 메인 고루틴이 종료되지 않도록 대기
}

위 코드를 실행하면 5초마다 현재 메모리 사용량과 고루틴 개수가 콘솔에 출력됩니다. 이를 통해 특정 시점에 메모리 사용량이 급증하는지, 아니면 꾸준히 증가하는 추세인지 파악할 수 있습니다. 특히 Alloc(현재 사용 중인 힙 메모리) 값이 계속 증가한다면 메모리 누수를 강력하게 의심할 수 있습니다.

💡 커스텀 프로파일링 점검 항목 💡
  • 주기적인 메모리 스냅샷 로깅:
    runtime.ReadMemStats
    를 활용하여
    Alloc
    ,
    TotalAlloc
    ,
    Goroutines
    등의 지표를 정기적으로 기록합니다.
  • 로그 분석 및 추세 파악: 기록된 로그를 시각화 도구(예: Grafana)로 그려보거나, 직접 눈으로 보면서 메모리 사용량의 증가 추이를 분석합니다.
  • 특정 이벤트와 메모리 변화 연관 짓기: 특정 기능 실행 후 메모리가 급증하거나, 특정 API 호출 시점에 메모리가 해제되지 않는 패턴을 찾아냅니다.

4.2 테이블: pprof vs. 커스텀 모니터링

pprof와 커스텀 모니터링은 서로 보완적인 관계를 가집니다. 각 도구의 장단점을 이해하고 적절히 활용하는 것이 중요합니다.

특징 pprof 커스텀 모니터링 (runtime.MemStats 등)
주요 강점
  • 특정 시점의 메모리 할당 스택 트레이스 시각화
  • 어떤 함수가 메모리를 할당했는지 직관적으로 파악
  • CPU, 고루틴 등 다양한 프로파일링 가능
  • 장시간에 걸친 메모리 사용량 추이 모니터링
  • 애플리케이션 상태와 메모리 변화를 연관 지어 분석
  • 프로덕션 환경에서 경량화된 실시간 지표 수집
주요 약점
  • 메모리 누수 원인(누가 참조하고 있는지) 직접 파악 어려움
  • 장시간 추이 분석에는 부적합 (스냅샷 기반)
  • 프로덕션 환경에서 상시 활성화 시 성능 오버헤드 발생 가능
  • 메모리 할당 스택 트레이스 제공 불가
  • 메모리 사용량의 근본적인 원인 (코드 라인)을 직접 지목하기 어려움
  • 구현 및 시각화 도구 연동 필요
활용 시나리오
  • 특정 시점에 메모리 사용량이 급증할 때 원인 분석
  • 개발/테스트 환경에서 특정 기능의 메모리 효율성 검증
  • 정기적인 성능 벤치마킹
  • 서비스 운영 중 메모리 누수 발생 여부 상시 모니터링
  • 장시간 실행되는 애플리케이션의 메모리 트렌드 분석
  • 알림(Alert) 시스템과 연동하여 임계치 초과 시 알림

5. 흔히 발생하는 Go 메모리 누수 패턴과 해결책

메모리 누수의 근본적인 원인을 찾기 위해 여러 도구를 사용했지만, 결국은 코드를 분석하고 수정해야 합니다. 제가 실제 경험하면서 자주 마주쳤던 메모리 누수 패턴과 그 해결책을 공유합니다. 이 부분을 잘 이해하고 여러분의 코드에 적용해 보세요.

5.1 슬라이스(Slice) 재참조 또는 잘못된 재사용

Go의 슬라이스는 매우 편리한 자료구조이지만, 잘못 사용하면 메모리 누수의 주범이 될 수 있습니다. 특히 슬라이스의 뒷부분을 잘라내어 새로운 슬라이스를 만드는 경우, 원래 슬라이스의 큰 배열을 계속 참조하고 있어서 실제로는 필요 없는 앞부분의 메모리가 해제되지 않는 경우가 많습니다.


// 예시: 슬라이스 누수 (문제 코드)
func processLargeSliceProblem(data []byte) []byte {
    // data는 매우 큰 슬라이스라고 가정
    // 우리는 이 중 일부만 필요하지만, 원본 배열을 계속 참조하게 됨
    return data[len(data)/2:] // 뒷부분만 사용
}

// 해결책: 새로운 슬라이스에 복사
func processLargeSliceSolution(data []byte) []byte {
    // 새로운 슬라이스를 만들어서 필요한 데이터만 복사
    // 이렇게 하면 원본 data의 큰 배열은 가비지 컬렉터가 회수할 수 있게 됨
    newSlice := make([]byte, len(data)/2)
    copy(newSlice, data[len(data)/2:])
    return newSlice
}

// 또 다른 슬라이스 누수: 슬라이스 재사용 시 capacity 관리 실패
var bufferPool = make(chan []byte, 10) // 버퍼 풀 (예시)

func getBuffer() []byte {
    select {
    case buf := <-bufferPool:
        return buf[:0] // 재사용 시 길이만 0으로 만들고 capacity는 그대로 유지
    default:
        return make([]byte, 0, 1024*1024) // 1MB capacity
    }
}

func putBuffer(buf []byte) {
    select {
    case bufferPool <- buf:
        // 성공적으로 버퍼 풀에 반환
    default:
        // 버퍼 풀이 가득 찼으면 버려짐 (GC 대상)
    }
}

설명:

data[len(data)/2:]

처럼 슬라이스를 자르면, 새로운 슬라이스는 원본 슬라이스의 기저 배열(underlying array)을 공유합니다. 만약 원본

data

슬라이스가 매우 컸다면, 새로운 슬라이스가 아무리 작아도 원본의 큰 메모리 공간이 가비지 컬렉터에 의해 회수되지 못합니다. 해결책은 필요한 부분만 새로운 슬라이스에 복사하여 사용하는 것입니다.

5.2 전역 변수(Global Variable) 또는 장수명 객체(Long-Lived Object)에 대한 참조

전역 변수나 오랫동안 살아있는 객체(예: 캐시, 연결 풀)가 필요 없는 객체를 계속 참조하고 있다면, 해당 객체는 가비지 컬렉터에 의해 정리되지 않습니다. 특히 맵(Map)이나 슬라이스 같은 컬렉션 타입에서 자주 발생합니다.


// 예시: 전역 맵을 사용한 메모리 누수 (문제 코드)
var cache = make(map[string][]byte)

func addToCache(key string, data []byte) {
    cache[key] = data // 한번 추가되면 영원히 남아있음
}

// 해결책: 캐시 크기 제한 또는 TTL(Time To Live) 구현
// 1. 캐시 라이브러리 사용 (예: groupcache, go-cache)
// 2. 직접 구현 시, LRU(Least Recently Used) 정책 등으로 오래된 항목 제거
type CacheEntry struct {
    Data    []byte
    Expires time.Time
}

var controlledCache = make(map[string]CacheEntry)

func addControlledCache(key string, data []byte, duration time.Duration) {
    controlledCache[key] = CacheEntry{
        Data:    data,
        Expires: time.Now().Add(duration),
    }
    // 주기적으로 만료된 항목을 제거하는 고루틴 필요
}

설명:

cache

맵에 한번 데이터를 넣으면, 해당 키를 삭제하지 않는 한 데이터는 계속 메모리에 남아있습니다. 만약 새로운 키가 계속 추가된다면 메모리 사용량은 무한히 증가할 것입니다. 이를 해결하기 위해 캐시 크기를 제한하거나, 일정 시간이 지나면 자동으로 만료되는 TTL(Time To Live) 기능을 구현해야 합니다.

5.3 고루틴(Goroutine) 누수

앞서 언급했듯이, 고루틴이 종료되지 않고 계속 쌓이면 메모리 누수처럼 보일 수 있습니다. 특히 채널(Channel)을 잘못 사용했을 때 자주 발생합니다.


// 예시: 고루틴 누수 (문제 코드)
func startWorkerProblem(inputChan <-chan int) {
    go func() {
        for {
            select {
            case data := <-inputChan: // 채널이 닫히지 않으면 여기서 계속 대기
                fmt.Printf("Processed: %d\n", data)
            // default나 context.Done()이 없으면 고루틴이 영원히 블록될 수 있음
            }
        }
    }()
}

// 해결책: context.Context 또는 채널 닫힘 활용
func startWorkerSolution(ctx context.Context, inputChan <-chan int) {
    go func() {
        defer fmt.Println("Worker 고루틴 종료")
        for {
            select {
            case data := <-inputChan:
                fmt.Printf("Processed: %d\n", data)
            case <-ctx.Done(): // 컨텍스트 취소 신호가 오면 종료
                return
            }
        }
    }()
}

설명:

startWorkerProblem

함수에서 생성된 고루틴은

inputChan

채널에 데이터가 없거나 채널이 닫히지 않으면

<-inputChan

부분에서 영원히 대기합니다. 이 고루틴은 절대 종료되지 않고 계속 메모리를 점유하게 됩니다. 해결책은

context.Context

를 사용하여 외부에서 고루틴에게 종료 신호를 보내거나,

inputChan

을 사용 후 닫아서

for range

문이 종료되도록 하는 것입니다.

Go 애플리케이션 메모리 누수 진단 및 해결: pprof를 넘어선 심층 분석과 커스텀 프로파일링 도구 활용 - silver grass, nature, walk, go to the memory bird, park

Image by hks0710 on Pixabay

6. 메모리 누수 해결 후 지속적인 관리 및 예방

메모리 누수를 성공적으로 진단하고 해결했다면, 그것으로 끝이 아닙니다. 앞으로 유사한 문제가 재발하지 않도록 지속적으로 관리하고 예방하는 노력이 필요합니다.

💡 지속적인 관리 및 예방 점검 항목 💡
  • 정기적인 프로파일링: 새로운 기능을 배포하거나 코드 변경이 있을 때마다 pprof를 통해 메모리 프로파일을 확인하는 습관을 들입니다.
  • 테스트 코드 강화: 메모리 누수를 유발할 수 있는 시나리오(예: 대량의 데이터 처리, 장시간 실행)에 대한 통합 테스트 및 부하 테스트를 작성하고, 이때 메모리 사용량을 모니터링합니다.
  • 모니터링 시스템 구축: 앞에서 언급한
    runtime.MemStats
    를 활용한 커스텀 모니터링 도구를 프로덕션 환경에 배포하고, 메모리 사용량에 대한 알림(Alert) 시스템을 구축합니다. 메모리 사용량이 특정 임계치를 넘으면 즉시 개발팀에 알림이 오도록 설정합니다.
  • 코드 리뷰: 다른 개발자의 코드를 리뷰할 때, 슬라이스, 맵, 채널, 고루틴 등 메모리 누수를 유발할 수 있는 패턴이 없는지 주의 깊게 확인합니다.
  • 라이브러리 선택 신중: 외부 라이브러리를 사용할 때는 해당 라이브러리의 메모리 관리 방식이나 성능 특성을 미리 검토합니다. 가능하다면
    pprof
    로 테스트해 보는 것도 좋습니다.

이런 점검 항목들을 꾸준히 실천한다면, Go 애플리케이션의 메모리 누수는 더 이상 여러분을 괴롭히지 않을 것입니다.

7. 마무리하며: 메모리 누수 탐정의 여정

Go 애플리케이션의 메모리 누수는 개발자에게 항상 도전적인 과제입니다. 저도 pprof라는 강력한 도구만으로는 해결하기 어려운 깊숙한 문제들을 겪으면서 많은 것을 배웠습니다. pprof로 넓은 범위를 스캔하고, 커스텀 프로파일링 도구로 미세한 변화를 추적하며, 마지막으로 흔한 누수 패턴들을 점검하는 과정은 마치 탐정이 범인을 쫓는 것과 같았습니다.

이 글에서 제가 공유한 실무 경험과 점검 항목들이 여러분의 Go 애플리케이션을 더욱 안정적이고 효율적으로 만드는 데 도움이 되기를 진심으로 바랍니다. 메모리 누수 진단은 단순히 문제를 해결하는 것을 넘어, Go 언어의 내부 동작과 메모리 관리 방식에 대한 이해를 깊게 하는 좋은 기회가 될 수 있습니다. 두려워하지 말고, 여러분의 애플리케이션을 더 나은 방향으로 이끌어 가세요!

혹시 여러분만의 특별한 메모리 누수 진단 팁이나 해결 경험이 있다면 댓글로 공유해 주세요. 함께 배우고 성장하는 개발 커뮤니티를 만들어가면 좋겠습니다!

📌 함께 읽으면 좋은 글

  • [임베디드 IoT] 느려터진 IoT 대시보드, 엣지에서 데이터 전처리로 다시 숨통 튼 이야기
  • [개발 도구] 개발 팀 생산성 2배로 올린 AI 어시스턴트 활용법: 쿼리, 인프라 스크립트 작성 혁신
  • [커리어 취업] 해외 테크 기업 취업, FAANG과 스타트업 사이의 진실은?

이 글이 도움이 되셨다면 공감(♥)댓글로 응원해 주세요!
궁금한 점이나 다루었으면 하는 주제가 있다면 댓글로 남겨주세요.

반응형